0
Total des articles
€0,00
Taxes incluses. Réductions et frais d’expédition calculés à l’étape du paiement.
Sous-total des produits
échantillons de couleur
(function () {
'use strict';
var PALETTE_ROLE = {
TRUE_NEUTRAL: 'true-neutral',
COLORED_NEUTRAL: 'colored-neutral',
ACCENT: 'accent'
};
var assetsCache = null;
var assetsPromise = null;
function escapeHtml(value) {
return String(value || '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
function loadAssetsConfig(url) {
if (assetsCache) return Promise.resolve(assetsCache);
if (assetsPromise) return assetsPromise;
assetsPromise = fetch(url, { credentials: 'same-origin' })
.then(function (response) {
if (!response.ok) throw new Error('Could not load outfit assets config');
return response.json();
})
.then(function (config) {
assetsCache = config;
return config;
})
.catch(function (error) {
assetsPromise = null;
throw error;
});
return assetsPromise;
}
function getSlotsWithColors(slots) {
return (slots || []).filter(function (slot) {
return slot && slot.color;
});
}
function getSlotsByRole(slots, role) {
return getSlotsWithColors(slots).filter(function (slot) {
return slot.role === role;
});
}
function getSlotMatch(slots, spec) {
if (typeof spec.paletteSlot === 'number') {
var paletteIndex = spec.paletteSlot - 1;
if (paletteIndex < 0 || !slots || paletteIndex >= slots.length) return null;
var paletteSlot = slots[paletteIndex];
return paletteSlot && paletteSlot.color ? paletteSlot : null;
}
var matches = getSlotsByRole(slots, spec.slotRole);
if (!matches.length) return null;
if (spec.slotRole === PALETTE_ROLE.TRUE_NEUTRAL && typeof spec.trueNeutralOrder === 'number') {
var ordered = matches.find(function (slot) {
return slot.trueNeutralOrder === spec.trueNeutralOrder;
});
if (ordered) return ordered;
matches.sort(function (a, b) {
return (a.trueNeutralOrder || 99) - (b.trueNeutralOrder || 99);
});
return matches[spec.trueNeutralOrder - 1] || matches[0];
}
if (spec.slotRole === PALETTE_ROLE.ACCENT && typeof spec.accentIndex === 'number') {
return matches[spec.accentIndex] || matches[0];
}
return matches[0];
}
function getGarmentLabel(layer, config) {
var garmentConfig = config && config.garments && config.garments[layer.garment];
var productName = layer.productName || (garmentConfig && garmentConfig.productName);
if (productName) return productName;
return layer.garment || '';
}
var GARMENT_RENDER_ORDER = {
top: 10,
pants: 20,
skirt: 20,
blossomSkirt: 20,
tidePants: 20,
dress: 20
};
function sortLayersForRendering(layers) {
return (layers || []).slice().sort(function (a, b) {
var orderA = GARMENT_RENDER_ORDER[a.garment] || 15;
var orderB = GARMENT_RENDER_ORDER[b.garment] || 15;
if (orderA !== orderB) return orderA - orderB;
return 0;
});
}
function buildOutfitPlans(slots, config) {
var recipes = (config && config.outfits) || [];
return recipes.map(function (recipe) {
var layers = [];
var missingSlots = [];
(recipe.layers || []).forEach(function (layerSpec) {
var slot = getSlotMatch(slots, layerSpec);
if (!slot) {
missingSlots.push(layerSpec);
return;
}
layers.push({
garment: layerSpec.garment,
productName: layerSpec.productName || null,
slot: slot,
color: slot.color
});
});
return {
id: recipe.id,
title: recipe.title,
layers: layers,
missingSlots: missingSlots,
ready: layers.length > 0 && missingSlots.length === 0
};
});
}
function isKnownModelSlug(modelSlug, config) {
var models = config.models || {};
if (!modelSlug) return false;
if (models[modelSlug] && models[modelSlug].baseUrl) return true;
var slugs = models.slugs;
if (Array.isArray(slugs)) return slugs.indexOf(modelSlug) !== -1;
return !!models[modelSlug];
}
function resolveModelSlug(modelSlug, config) {
if (isKnownModelSlug(modelSlug, config)) return modelSlug;
return config.defaultModel || modelSlug || 'aino';
}
function colorSlugFromName(name) {
return String(name || '')
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
function getColorSlug(color) {
if (!color) return '';
if (color.slug) return color.slug;
return colorSlugFromName(color.name);
}
function applyUrlTemplate(template, values) {
return String(template).replace(/\{(\w+)\}/g, function (match, key) {
return values[key] != null && values[key] !== '' ? values[key] : match;
});
}
function resolveGarmentUrl(garmentType, color, config) {
var garment = config.garments && config.garments[garmentType];
if (!garment || !color) return '';
var vendorCode = color.vendorCode;
var entry = garment.byVendorCode && garment.byVendorCode[vendorCode];
if (entry && entry.url) return entry.url;
if (garment.urlTemplate) {
var colorSlug = getColorSlug(color);
if (!colorSlug) return '';
var url = applyUrlTemplate(garment.urlTemplate, {
colorSlug: colorSlug,
vendorCode: vendorCode || '',
productSlug: garment.productSlug || garmentType || ''
});
return url.indexOf('{') === -1 ? url : '';
}
if (garment.templateUrl) return garment.templateUrl;
return '';
}
function getModelBaseUrl(config, modelSlug) {
var resolvedModel = resolveModelSlug(modelSlug, config);
var models = config.models || {};
var entry = models[resolvedModel];
if (entry && entry.baseUrl) return entry.baseUrl;
if (models.urlTemplate) {
var url = applyUrlTemplate(models.urlTemplate, { modelSlug: resolvedModel });
return url.indexOf('{') === -1 ? url : '';
}
return '';
}
function getFallbackDressUrl(config) {
return config.fallbackGarment && config.fallbackGarment.dress;
}
function planNeedsGarmentFallback(plan, config) {
return plan.layers.some(function (layer) {
return !resolveGarmentUrl(layer.garment, layer.color, config);
});
}
function canCompositeFullOutfit(plan, config) {
return !planNeedsGarmentFallback(plan, config);
}
function canRenderOutfit(plan, config, modelSlug) {
if (!plan.layers.length) return false;
if (canCompositeFullOutfit(plan, config)) return true;
return !!(getModelBaseUrl(config, modelSlug) && getFallbackDressUrl(config));
}
function buildOutfitStack(plan, config, modelSlug) {
var backgroundUrl = config.background && config.background.url;
var fallbackColor = (plan.layers[0] && plan.layers[0].color && plan.layers[0].color.hex) || '#f4f1ec';
var modelUrl = getModelBaseUrl(config, modelSlug);
var stackLayers = [];
var garmentLayers = [];
if (backgroundUrl) {
stackLayers.push({ url: backgroundUrl });
} else {
stackLayers.push({ color: fallbackColor });
}
if (modelUrl) stackLayers.push({ url: modelUrl });
sortLayersForRendering(plan.layers).forEach(function (layer) {
var garmentUrl = resolveGarmentUrl(layer.garment, layer.color, config);
if (garmentUrl) {
garmentLayers.push({
url: garmentUrl,
label: getGarmentLabel(layer, config) + ' (' + layer.color.name + ')'
});
}
});
var usedFallback = false;
var usedPartial = false;
if (garmentLayers.length === plan.layers.length) {
stackLayers = stackLayers.concat(garmentLayers);
} else if (garmentLayers.length > 0) {
stackLayers = stackLayers.concat(garmentLayers);
usedPartial = true;
} else {
var dressUrl = getFallbackDressUrl(config);
if (dressUrl) {
stackLayers.push({ url: dressUrl });
usedFallback = true;
}
}
return {
layers: stackLayers.filter(function (layer) {
return layer.url || layer.color;
}),
usedFallback: usedFallback,
usedPartial: usedPartial
};
}
function renderStackHtml(stack) {
return (stack.layers || []).map(function (layer) {
if (layer.color) {
return '';
}
return '
';
}).join('');
}
function loadImage(url) {
return new Promise(function (resolve, reject) {
if (!url) {
reject(new Error('Missing image url'));
return;
}
var image = new Image();
image.crossOrigin = 'anonymous';
image.onload = function () { resolve(image); };
image.onerror = function () { reject(new Error('Could not load image: ' + url)); };
image.src = url;
});
}
function drawCover(ctx, image, width, height) {
var scale = Math.max(width / image.width, height / image.height);
var drawWidth = image.width * scale;
var drawHeight = image.height * scale;
var x = (width - drawWidth) / 2;
var y = (height - drawHeight) / 2;
ctx.drawImage(image, x, y, drawWidth, drawHeight);
}
function compositeFallbackOutfit(plan, config, modelSlug) {
var dressUrl = getFallbackDressUrl(config);
var modelUrl = getModelBaseUrl(config, modelSlug);
if (!dressUrl || !modelUrl) {
return Promise.reject(new Error('Missing fallback garment or model'));
}
var canvasWidth = (config.canvas && config.canvas.width) || 1200;
var canvasHeight = (config.canvas && config.canvas.height) || 1600;
var canvas = document.createElement('canvas');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
var ctx = canvas.getContext('2d');
var backgroundUrl = config.background && config.background.url;
var fallbackColor = (plan.layers[0] && plan.layers[0].color && plan.layers[0].color.hex) || '#f4f1ec';
function drawStack(model, dress, background) {
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
if (background) {
drawCover(ctx, background, canvasWidth, canvasHeight);
} else {
ctx.fillStyle = fallbackColor;
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
}
drawCover(ctx, model, canvasWidth, canvasHeight);
drawCover(ctx, dress, canvasWidth, canvasHeight);
return canvas.toDataURL('image/jpeg', 0.92);
}
return Promise.all([loadImage(modelUrl), loadImage(dressUrl)])
.then(function (images) {
var model = images[0];
var dress = images[1];
if (!backgroundUrl) {
return drawStack(model, dress, null);
}
return loadImage(backgroundUrl).then(function (background) {
return drawStack(model, dress, background);
});
});
}
function compositeLayeredOutfit(plan, config, modelSlug) {
var canvasWidth = (config.canvas && config.canvas.width) || 1200;
var canvasHeight = (config.canvas && config.canvas.height) || 1600;
var canvas = document.createElement('canvas');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
var ctx = canvas.getContext('2d');
var modelUrl = getModelBaseUrl(config, modelSlug);
var backgroundUrl = config.background && config.background.url;
var fallbackColor = (plan.layers[0] && plan.layers[0].color && plan.layers[0].color.hex) || '#f4f1ec';
var urls = [];
var hasBackground = !!backgroundUrl;
if (hasBackground) urls.push(backgroundUrl);
urls.push(modelUrl);
sortLayersForRendering(plan.layers).forEach(function (layer) {
urls.push(resolveGarmentUrl(layer.garment, layer.color, config));
});
return Promise.all(urls.map(loadImage)).then(function (images) {
var index = 0;
var background = hasBackground ? images[index++] : null;
var model = images[index++];
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
if (background) {
drawCover(ctx, background, canvasWidth, canvasHeight);
} else {
ctx.fillStyle = fallbackColor;
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
}
drawCover(ctx, model, canvasWidth, canvasHeight);
sortLayersForRendering(plan.layers).forEach(function () {
var garmentImage = images[index++];
drawCover(ctx, garmentImage, canvasWidth, canvasHeight);
});
return canvas.toDataURL('image/jpeg', 0.92);
});
}
function compositeOutfit(plan, config, modelSlug) {
var stack = buildOutfitStack(plan, config, modelSlug);
if (!stack.layers.length) {
return Promise.reject(new Error('No outfit layers to render'));
}
var canvasWidth = (config.canvas && config.canvas.width) || 1200;
var canvasHeight = (config.canvas && config.canvas.height) || 1600;
var canvas = document.createElement('canvas');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
var ctx = canvas.getContext('2d');
var imageLayers = stack.layers.filter(function (layer) {
return !!layer.url;
});
return Promise.all(imageLayers.map(function (layer) {
return loadImage(layer.url);
})).then(function (images) {
var imageIndex = 0;
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
stack.layers.forEach(function (layer) {
if (layer.color) {
ctx.fillStyle = layer.color;
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
return;
}
if (!layer.url) return;
drawCover(ctx, images[imageIndex++], canvasWidth, canvasHeight);
});
return canvas.toDataURL('image/jpeg', 0.92);
});
}
function renderPlaceholderCard(plan, config) {
var swatches = plan.layers.map(function (layer) {
return (
''
);
}).join('');
return (
'' +
'' +
' '
);
}
function renderLoadingCard(plan) {
return (
'' +
'' +
' '
);
}
function renderStackCard(plan, stack, config) {
return (
'' +
' '
);
}
function renderEmptyCard(plan) {
return (
'' +
' '
);
}
function renderGallery(container, plans, config, modelSlug) {
if (!container) return Promise.resolve();
container.innerHTML = plans.map(function (plan) {
if (!plan.layers.length) return renderEmptyCard(plan);
if (!canRenderOutfit(plan, config, modelSlug)) return renderPlaceholderCard(plan, config);
var stack = buildOutfitStack(plan, config, modelSlug);
if (!stack.layers.length) return renderPlaceholderCard(plan, config);
return renderStackCard(plan, stack, config);
}).join('');
return Promise.resolve();
}
function render(container, options) {
options = options || {};
if (!container) return Promise.resolve();
var assetsUrl = options.assetsUrl || '';
if (!assetsUrl) {
container.innerHTML = '
' + swatches + '
' +
'' + escapeHtml(plan.title) + '
' + '' +
renderStackHtml(stack) +
'
' +
'' + escapeHtml(plan.title) + '
' + '' + escapeHtml(plan.title) + '
' + 'Outfit assets are not configured yet.
'; return Promise.resolve(); } container.innerHTML = 'Loading outfit previews...
'; return loadAssetsConfig(assetsUrl) .then(function (config) { var plans = buildOutfitPlans(options.slots || [], config); return renderGallery(container, plans, config, options.modelSlug || config.defaultModel); }) .catch(function () { container.innerHTML = 'Could not load outfit previews. Upload palette-outfit-assets.json to your theme assets.
