// ==UserScript==
// @name Anywear 手机中文版与摄像头选择
// @namespace https://anywear.decart.ai/
// @version 0.1
// @description 自动全屏打开试穿,汉化 Anywear,支持摄像头选择、多件服装上传切换和截图保存。
// @license MIT
// @match https://anywear.decart.ai/*
// @run-at document-start
// @grant none
// @inject-into page
// @sandbox raw
// ==/UserScript==
(function () {
'use strict';
const CAMERA_SELECT_ID = 'aw-zh-camera-select';
// 必须尽早运行:官方页面在点击“启用摄像头”时调用 getUserMedia,
// 这里读取选择框并替换其写死的 facingMode: "user"。
function installCameraInterceptor() {
const code = `(() => {
if (window.__awZhCameraPatched) return;
window.__awZhCameraPatched = true;
const mediaDevices = navigator.mediaDevices;
if (!mediaDevices || !mediaDevices.getUserMedia) return;
const original = mediaDevices.getUserMedia.bind(mediaDevices);
mediaDevices.getUserMedia = async function (constraints) {
const next = constraints ? { ...constraints } : {};
const originalVideo = next.video;
if (originalVideo !== false) {
const video = originalVideo && typeof originalVideo === 'object'
? { ...originalVideo }
: {};
const select = document.getElementById('${CAMERA_SELECT_ID}');
const choice = select ? select.value : 'front';
delete video.facingMode;
delete video.deviceId;
if (choice.startsWith('device:')) {
video.deviceId = { exact: choice.slice(7) };
} else if (choice === 'back') {
video.facingMode = { ideal: 'environment' };
} else {
video.facingMode = { ideal: 'user' };
}
next.video = video;
}
const stream = await original(next);
window.dispatchEvent(new CustomEvent('aw-zh-camera-opened'));
return stream;
};
})();`;
const script = document.createElement('script');
script.textContent = code;
(document.documentElement || document).appendChild(script);
script.remove();
}
installCameraInterceptor();
const translations = new Map([
['Try clothes on,', '试穿任意服装,'],
['anywhere.', '随时随地。'],
['Drag any clothing item from any online store and see it on you, instantly.', '从任意在线商店选择服装,即时查看上身效果。'],
['Add to Chrome', '添加到 Chrome'],
['Try now', '立即试穿'],
['Free during Beta.', 'Beta 期间免费'],
['Turn on your camera.', '打开摄像头'],
['Enable camera', '启用摄像头'],
['Starting…', '正在启动…'],
['Drag a product onto yourself.', '选择一件服装进行试穿'],
['For best results — remove any thick jacket or outer layer for the most accurate try-on.', '为获得更好效果,请先脱下厚外套或外层服装。'],
['Connecting…', '正在连接…'],
['Connecting to the studio…', '正在连接 AI 试衣间…'],
['Dressing…', '正在更换服装…'],
['Continue →', '继续 →'],
['Install on every online-store.', '在任意在线商店使用'],
['Add Anywear to Chrome and this exact try-on appears right on every product page — Zara, ASOS, Amazon and beyond. Drag any item onto yourself and see how it looks before you buy.', '将 Anywear 添加到 Chrome,即可在 Zara、ASOS、Amazon 等商品页面使用试穿功能。'],
['Try another outfit', '试穿另一件'],
['Own a store?', '拥有在线商店?'],
['Add virtual try-on to your website →', '将虚拟试穿添加到您的网站 →'],
['Privacy Policy', '隐私政策'],
['Terms of Service', '服务条款'],
['Camera permission denied. Enable it and retry.', '摄像头权限被拒绝,请授权后重试。'],
['Could not start. Please try again.', '无法启动,请重试。'],
['Connection issue — please try again.', '连接异常,请重试。'],
]);
function translateText(root) {
if (!root) return;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
const nodes = [];
while (walker.nextNode()) nodes.push(walker.currentNode);
for (const node of nodes) {
const parent = node.parentElement;
if (!parent || /^(SCRIPT|STYLE|TEXTAREA|CODE)$/i.test(parent.tagName)) continue;
const trimmed = node.nodeValue.trim();
const translated = translations.get(trimmed);
if (translated) node.nodeValue = node.nodeValue.replace(trimmed, translated);
}
}
function cameraLabel(device, index) {
if (device.label) return device.label;
return `摄像头 ${index + 1}(授权后显示名称)`;
}
async function refreshCameraList(select) {
if (!navigator.mediaDevices?.enumerateDevices) return;
const previous = select.value;
const devices = (await navigator.mediaDevices.enumerateDevices())
.filter(device => device.kind === 'videoinput');
select.querySelectorAll('option[data-device]').forEach(option => option.remove());
devices.forEach((device, index) => {
const option = document.createElement('option');
option.value = `device:${device.deviceId}`;
option.textContent = cameraLabel(device, index);
option.dataset.device = 'true';
select.appendChild(option);
});
if ([...select.options].some(option => option.value === previous)) {
select.value = previous;
}
updateOutputMirroring();
}
function updateOutputMirroring() {
const select = document.getElementById(CAMERA_SELECT_ID);
const output = document.getElementById('t-output');
if (!select || !output) return;
// 前摄保留自拍镜像,后摄取消官网强制添加的镜像。
output.classList.toggle('aw-zh-no-mirror', isBackCameraSelected());
}
function isBackCameraSelected() {
const select = document.getElementById(CAMERA_SELECT_ID);
if (!select) return false;
const selectedText = select.selectedOptions?.[0]?.textContent || '';
return select.value === 'back' || /back|rear|environment|后置|背面/i.test(selectedText);
}
function flipImageHorizontally(dataUrl) {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const context = canvas.getContext('2d');
context.translate(canvas.width, 0);
context.scale(-1, 1);
context.drawImage(image, 0, 0);
resolve(canvas.toDataURL('image/png'));
};
image.onerror = reject;
image.src = dataUrl;
});
}
function addControls() {
if (document.getElementById(CAMERA_SELECT_ID)) return;
const host = document.querySelector('.cam-hero') || document.body;
const controls = document.createElement('div');
controls.id = 'aw-zh-controls';
controls.innerHTML = `
请在点击“启用摄像头”之前选择。
`;
const style = document.createElement('style');
style.textContent = `
#aw-zh-controls { width:min(92%,420px); display:grid; gap:8px; color:#fff; font-family:inherit; }
#aw-zh-controls label { font-size:15px; font-weight:600; }
#aw-zh-controls select, #aw-zh-controls button { min-height:44px; border-radius:10px; border:1px solid rgba(255,255,255,.35); padding:0 12px; font-size:15px; }
#aw-zh-controls select { color:#170404; background:#fff; }
#aw-zh-controls button { color:#fff; background:rgba(255,255,255,.14); }
#aw-zh-controls small { color:rgba(255,255,255,.75); }
/* 将官网居中弹窗改成手机全屏试衣间。 */
#tryon-modal.tryon-modal { padding:0 !important; width:100vw; height:100dvh; }
#tryon-modal .tryon-backdrop { display:none !important; }
#tryon-modal .tryon-dialog { width:100vw !important; max-width:none !important; height:100dvh !important; max-height:none !important; border:0 !important; border-radius:0 !important; padding-top:max(22px,env(safe-area-inset-top)) !important; padding-bottom:max(22px,env(safe-area-inset-bottom)) !important; }
#tryon-modal .tryon-close { display:none !important; }
#tryon-modal .tryon-head { display:none !important; margin:0 !important; }
#t-output.aw-zh-no-mirror { transform:none !important; }
/* 隐藏官网自带的四件示例服装;节点仍保留给内部 setImage 调用。 */
#tstep2 .t-garments { display:none !important; }
#tstep2 .t-tip { display:none !important; }
#tstep2 .t-s2 { width:100% !important; }
#tstep2 .t-cam-right { width:100% !important; flex:1 1 100% !important; }
#aw-zh-closet { position:fixed; left:12px; right:12px; bottom:max(12px,env(safe-area-inset-bottom)); z-index:10010; display:flex; align-items:center; gap:8px; padding:8px; border-radius:14px; background:rgba(255,255,252,.96); box-shadow:0 8px 30px rgba(0,0,0,.28); backdrop-filter:blur(12px); }
#aw-zh-upload { flex:0 0 auto; border:0; border-radius:999px; padding:11px 15px; background:#170404; color:#fff; font-size:14px; white-space:nowrap; }
#aw-zh-capture { position:absolute; right:12px; bottom:12px; z-index:5; border:0; border-radius:999px; padding:10px 15px; background:#c65a92; color:#fff; box-shadow:0 4px 14px rgba(0,0,0,.28); font-family:inherit; font-size:14px; white-space:nowrap; cursor:pointer; }
#aw-zh-garments { display:flex; flex:1; gap:7px; overflow-x:auto; overscroll-behavior-x:contain; scrollbar-width:thin; }
.aw-zh-garment { position:relative; flex:0 0 54px; width:54px; height:68px; padding:0; overflow:hidden; border:2px solid transparent; border-radius:9px; background:#eee; }
.aw-zh-garment.active { border-color:#c65a92; }
.aw-zh-garment img { width:100%; height:100%; object-fit:cover; display:block; }
.aw-zh-garment .remove { position:absolute; top:1px; right:1px; width:19px; height:19px; padding:0; border:0; border-radius:50%; background:rgba(0,0,0,.72); color:#fff; font-size:13px; line-height:19px; }
`;
document.head.appendChild(style);
host.prepend(controls);
const select = controls.querySelector(`#${CAMERA_SELECT_ID}`);
select.addEventListener('change', updateOutputMirroring);
controls.querySelector('#aw-zh-refresh-camera').addEventListener('click', () => refreshCameraList(select));
refreshCameraList(select).catch(console.warn);
updateOutputMirroring();
}
function addGarmentUpload() {
if (document.getElementById('aw-zh-upload')) return;
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/jpeg,image/png,image/webp';
input.multiple = true;
input.hidden = true;
const closet = document.createElement('div');
closet.id = 'aw-zh-closet';
const button = document.createElement('button');
button.id = 'aw-zh-upload';
button.type = 'button';
button.textContent = '+添加衣服';
button.addEventListener('click', () => input.click());
const garmentList = document.createElement('div');
garmentList.id = 'aw-zh-garments';
garmentList.setAttribute('aria-label', '我的衣服');
const captureButton = document.createElement('button');
captureButton.id = 'aw-zh-capture';
captureButton.type = 'button';
captureButton.textContent = '截图保存';
async function captureCurrentFrame() {
const video = document.getElementById('t-output');
if (!video || !video.videoWidth || !video.videoHeight || video.readyState < 2) {
alert('AI 试穿画面尚未准备好,请稍后再截图。');
return;
}
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const context = canvas.getContext('2d');
// 官网用 CSS 镜像视频;保存结果与用户屏幕上看到的方向保持一致。
const transform = getComputedStyle(video).transform;
const mirrored = transform && transform !== 'none' &&
(/^matrix\(-1(?:,|\s)/.test(transform) || /^matrix3d\(-1(?:,|\s)/.test(transform));
if (mirrored) {
context.translate(canvas.width, 0);
context.scale(-1, 1);
}
context.drawImage(video, 0, 0, canvas.width, canvas.height);
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png', 1));
if (!blob) {
alert('截图生成失败,请重试。');
return;
}
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = `anywear-${stamp}.png`;
// 直接保存到浏览器的默认下载目录,不再打开系统分享面板。
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(url), 30000);
}
captureButton.addEventListener('click', captureCurrentFrame);
async function applyUploadedGarment(dataUrl, item) {
const image = document.querySelector('#tstep2 .g-thumb img');
if (!image) {
alert('请先点击“立即试穿”并启用摄像头。');
return;
}
document.querySelectorAll('.aw-zh-garment').forEach(el => el.classList.remove('active'));
item.classList.add('active');
// 前摄输出采用自拍镜像,因此仅将送给模型的服装参考图水平翻转;
// 用户衣柜里的原始缩略图保持不变。后摄直接发送原图。
image.src = isBackCameraSelected() ? dataUrl : await flipImageHorizontally(dataUrl);
image.closest('.g-thumb')?.click();
}
function addGarment(file) {
const reader = new FileReader();
reader.onload = () => {
const dataUrl = String(reader.result);
const item = document.createElement('button');
item.type = 'button';
item.className = 'aw-zh-garment';
item.title = file.name;
const preview = document.createElement('img');
preview.src = dataUrl;
preview.alt = file.name;
const remove = document.createElement('span');
remove.className = 'remove';
remove.textContent = '×';
remove.setAttribute('role', 'button');
remove.setAttribute('aria-label', `删除 ${file.name}`);
remove.addEventListener('click', event => {
event.preventDefault();
event.stopPropagation();
item.remove();
});
item.append(preview, remove);
item.addEventListener('click', () => applyUploadedGarment(dataUrl, item));
garmentList.appendChild(item);
};
reader.readAsDataURL(file);
}
input.addEventListener('change', () => {
const files = [...(input.files || [])];
files.forEach(addGarment);
// 清空后,同一批文件下次仍可再次选择。
input.value = '';
});
const cameraPanel = document.querySelector('#tstep2 .t-cam-panel');
if (cameraPanel) cameraPanel.appendChild(captureButton);
else closet.appendChild(captureButton);
closet.append(button, garmentList);
document.body.append(input, closet);
}
function openTryOnAutomatically() {
let opened = false;
let attempts = 0;
function forceOpen() {
if (opened) return true;
const modal = document.getElementById('tryon-modal');
if (!modal) return false;
// 优先走官网逻辑;隔离环境中访问不到该函数时直接打开 DOM。
try {
if (typeof window.openTryModal === 'function') window.openTryModal();
} catch (error) {
console.warn('[Anywear 中文版] 官方打开函数不可用,改用直接打开。', error);
}
modal.classList.add('open');
modal.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
// 确保第一步可见,避免官网修改初始化顺序后出现空白全屏层。
const firstStep = document.getElementById('tstep1');
if (firstStep && !modal.querySelector('.tstep.active')) firstStep.classList.add('active');
opened = modal.classList.contains('open');
return opened;
}
if (forceOpen()) return;
const timer = setInterval(() => {
attempts += 1;
if (forceOpen() || attempts >= 100) clearInterval(timer);
}, 100);
// 部分手机用户脚本管理器会延迟到 load/pageshow 阶段才注入。
window.addEventListener('load', forceOpen, { once: true });
window.addEventListener('pageshow', forceOpen, { once: true });
const observer = new MutationObserver(() => {
if (forceOpen()) observer.disconnect();
});
observer.observe(document.documentElement, { childList: true, subtree: true });
}
function initialize() {
translateText(document.body);
addControls();
addGarmentUpload();
openTryOnAutomatically();
const observer = new MutationObserver(records => {
records.forEach(record => record.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) translateText(node);
}));
translateText(document.body);
});
observer.observe(document.body, { childList: true, subtree: true });
window.addEventListener('aw-zh-camera-opened', () => {
const select = document.getElementById(CAMERA_SELECT_ID);
updateOutputMirroring();
if (select) refreshCameraList(select).catch(console.warn);
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize, { once: true });
} else {
initialize();
}
})();