Skip to main content

Custom Engraving Tool

Comprehensive documentation for the custom product engraving system integrated into Cold Steel's BigCommerce store.

Overview

The engraving tool allows customers to personalize knives and other products with custom text or images. It provides a live canvas preview and integrates with GSM Outdoors' engraving API.

Architecture

Components

assets/js/theme/engraving/
├── engraving-tool.js # Main engraving tool class
├── engraver.js # UI helpers and event handlers
├── engraver-utils.js # Utility functions
├── engraver-image-utils.js # Image manipulation
├── engraver-text-on-image.js # Text rendering
├── engraver-http.js # API communication
├── engraver-add-to-cart.js # Cart integration
├── engraver-lineitems-ids.js # Line item management
└── lineitems/ # Line item helpers

Configuration Files

assets/js/
├── engraving-config.json # Product IDs and option mappings
├── engraverfonts.json # Available fonts
└── engraverImageCat.json # Clip art categories

Dependencies

  • HTML5 Canvas API - Image rendering and manipulation
  • jQuery - DOM manipulation and event handling
  • Fetch API - HTTP requests to engraving API
  • BigCommerce Stencil Utils - Cart operations

Configuration

Engraving Product Setup

The engraving tool is configured per product in engraving-config.json:

{
"live": {
"product_id": 726,
"engraving_text": {
"option_id": 1
},
"font": {
"option_id": 2,
"data": {
"Black Chancery": "98",
"Courier": "99",
"Helvetica": "100",
"Impact": "101",
"Phoenix Script": "102",
"Woodman Serif": "103",
"VarelaRound": "104",
"Times New Roman": "105",
"TaticSans": "106",
"Stencil": "107",
"Lucida-Handwriting": "108",
"Cooper Black": "109",
"Conneqt": "110",
"Bill Smith": "111",
"Arial": "112"
}
},
"font_size": {
"option_id": 3,
"data": {
"10px": "113",
"12px": "114",
"14px": "115",
"16px": "116",
"18px": "117",
"22px": "138",
"26px": "139",
"30px": "140",
"34px": "141",
"38px": "142",
"SM": "143",
"MD": "144",
"LG": "145",
"XL": "146",
"2XL": "147"
}
},
"formatting": {
"option_id": 4,
"data": {
"Normal": "118",
// Additional formatting options...
}
}
}
}

Product-Specific Settings

Each engraving-enabled product has configuration stored in:

window.productData = {
engraving_price: 10.00,
main_product_price: 49.99,
main_product_name: "Product Name",
product_is_black: true,
maxWidth: 400,
coordinates: {
text: { x: 400, y: 510 },
image: { x: 400, y: 510 }
},
sogLogo: "path/to/logo.png",
saveMockupUrl: "api/save-mockup",
saveTextAsImageUrl: "api/save-text-image",
deleteUploadedImageUrl: "api/delete-image",
saveUploadedImageUrl: "api/save-image"
};

User Interface

Activation

The engraving tool is activated via checkbox on product page:

<input type="checkbox" id="add_engraving" />
<label for="add_engraving">Add Engraving ($10.00)</label>

When checked:

  1. Engraving panel slides into view
  2. Canvas preview initializes
  3. Font carousel loads
  4. Option selectors activate

UI Components

1. Engraving Type Selector

<div class="engraving-type-tabs">
<button data-type="text">Text Engraving</button>
<button data-type="image">Image Engraving</button>
</div>

2. Text Engraving Panel

  • Text Input: Multi-line textarea for engraving text
  • Font Selector: Carousel of 14 font options
  • Font Size: Dropdown or preset sizes (SM, MD, LG, XL, 2XL)
  • Formatting: Bold, Italic, Underline toggles
  • Alignment: Left, Center, Right

3. Image Engraving Panel

  • Clip Art Gallery: Grid of pre-approved designs
  • Upload Option: File input for custom images (JPG, PNG)
  • Image Preview: Thumbnail of selected image
  • Image Controls: Scale, rotate, position

4. Live Canvas Preview

<div id="engraveApp">
<canvas id="canvas" width="800" height="600"></canvas>
</div>

Features:

  • Real-time text rendering
  • Drag-and-drop positioning
  • Zoom controls
  • Background product image overlay
  • Engraving area boundaries

5. Progress Bar

Step-by-step wizard interface:

  1. Choose engraving type
  2. Enter text or select image
  3. Customize font/formatting
  4. Review and confirm
  5. Add to cart

Implemented with Slick Carousel:

$('.font-face-carousel').slick({
slidesToShow: 5,
slidesToScroll: 1,
arrows: true,
responsive: [
{
breakpoint: 768,
settings: { slidesToShow: 3 }
},
{
breakpoint: 480,
settings: { slidesToShow: 2 }
}
]
});

Each font displays:

  • Font name
  • Preview text in that font
  • Selection radio button

Canvas Rendering

EngravingTool Class

export default class EngravingTool {
constructor(product_data, canvas_id, productPage) {
this.canvas = document.getElementById(canvas_id);
this.ctx = this.canvas.getContext("2d");
this.product_data = product_data;

// Initialize canvas
this.canvasWidth = $(this.canvas).width();
this.canvasHeight = $(this.canvas).height();

// Default coordinates
this.coordinates = product_data.coordinates;

// Maximum engraving width
this.maximumWidth = parseInt(product_data.maxWidth) || 400;
}
}

Text Rendering

// engraver-text-on-image.js
export function saveTextAsImage(text, font, fontSize, formatting) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');

// Set font properties
let fontWeight = formatting.bold ? 'bold' : 'normal';
let fontStyle = formatting.italic ? 'italic' : 'normal';
ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${font}`;

// Measure text
const metrics = ctx.measureText(text);
const textWidth = metrics.width;

// Set canvas size
canvas.width = textWidth;
canvas.height = fontSize * 1.2;

// Apply formatting
if (formatting.underline) {
// Draw underline
}

// Draw text
ctx.fillText(text, 0, fontSize);

return canvas.toDataURL('image/png');
}

Image Processing

// engraver-image-utils.js

export function createImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous';
img.onload = () => resolve(img);
img.onerror = reject;
img.src = url;
});
}

export function invertImageColors(imageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i]; // Red
data[i + 1] = 255 - data[i + 1]; // Green
data[i + 2] = 255 - data[i + 2]; // Blue
}
return imageData;
}

export function whiteToTransparent(imageData) {
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
if (data[i] > 250 && data[i + 1] > 250 && data[i + 2] > 250) {
data[i + 3] = 0; // Make white pixels transparent
}
}
return imageData;
}

Drag-and-Drop Positioning

canvas.addEventListener('mousedown', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;

// Check if click is on text/image
if (isClickOnEngraving(x, y)) {
this.dragOk = true;
this.dragCoordinates = { x, y };
}
});

canvas.addEventListener('mousemove', (e) => {
if (!this.dragOk) return;

const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;

// Update engraving position
this.coordinates.text.x += x - this.dragCoordinates.x;
this.coordinates.text.y += y - this.dragCoordinates.y;

this.dragCoordinates = { x, y };
this.redrawCanvas();
});

GSM Outdoors API Integration

API Endpoints

The engraving tool communicates with GSM Outdoors' engraving service:

// engraving-tool.js
constructor(product_data, canvas_id, productPage) {
// Environment detection
this.apiUrlDomain = isDevelopment()
? 'https://engraving-test.gsmoutdoors.com'
: 'https://engraving.gsmoutdoors.com';

// Override for specific stores
if (window.location.hostname === 'cold-steel-knives-sandbox.mybigcommerce.com') {
this.apiUrlDomain = 'https://engraving-test.gsmoutdoors.com';
}
}

API Operations

1. Save Mockup

POST /api/save-mockup
Body: {
store_id: "abc123",
cart_id: "xyz789",
product_id: 726,
mockup_image: "data:image/png;base64,..."
}
Response: {
success: true,
mockup_url: "https://..."
}

2. Save Text as Image

POST /api/save-text-image
Body: {
text: "COLD STEEL",
font: "Black Chancery",
fontSize: 24,
formatting: {
bold: false,
italic: false,
underline: false
}
}
Response: {
image_url: "https://..."
}

3. Upload Custom Image

POST /api/save-image
Body: FormData with image file
Response: {
image_url: "https://..."
}

4. Delete Uploaded Image

DELETE /api/delete-image
Body: {
image_url: "https://..."
}
Response: {
success: true
}

HTTP Helper

// engraver-http.js
export function testCartId(cartId, storeId, apiDomain) {
return fetch(`${apiDomain}/api/test-cart`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ cart_id: cartId, store_id: storeId })
})
.then(response => response.json());
}

Add to Cart Integration

Cart Item Structure

When adding an engraved product:

// engraver-add-to-cart.js
export default function addEngravingToCart(engravingData, mainProductId) {
const lineItems = [
{
product_id: mainProductId,
quantity: 1,
option_selections: [
{
option_id: 1,
option_value: engravingData.text
},
{
option_id: 2,
option_value: engravingData.font_id
},
{
option_id: 3,
option_value: engravingData.size_id
}
]
},
{
product_id: window.engravingSettings.live.product_id,
quantity: 1,
parent_id: mainProductId // Link engraving to product
}
];

return utils.api.cart.itemAdd(lineItems);
}

Cart Preview Update

After adding to cart:

  1. Cart modal displays
  2. Shows main product + engraving as linked items
  3. Displays mockup image
  4. Shows engraving details (text, font, size)

Quantity Synchronization

// cart.js
checkEngravingQuantity(itemId, newQty) {
// Find associated engraving line item
const engravingItem = this.bulkKnifeEngravings.find(
e => e.parent_id === itemId
);

if (engravingItem) {
// Update engraving quantity to match product
utils.api.cart.itemUpdate(engravingItem.id, newQty);
}
}

Bulk Engraving Removal

maybeProcessBulkEngravingRemoval() {
// When main product is removed, also remove engraving
$('[data-cart-itemid]').on('click', '.cart-remove', (e) => {
const itemId = $(e.currentTarget).data('cart-itemid');
const engravingItem = this.bulkKnifeEngravings.find(
e => e.parent_id === itemId
);

if (engravingItem) {
utils.api.cart.itemRemove(engravingItem.id);
}
});
}

Product Content Management

Per-Product Configuration

Content stored in content/ directory by SKU:

content/
├── CS-20NPJAA/ # Product SKU folders
├── CS-20NQX/
├── CS-20NQXZ/
├── CS-49LCK/
├── CS-80PGTK/
├── CS-90AXG/
├── CS-92SFX/
└── product/ # Shared product assets

Each product folder contains:

  • Background images for canvas
  • Pattern/engraving area overlays
  • Clip art specific to that product
  • Configuration JSON

Clip Art Library

// engraverImageCat.json
{
"categories": [
{
"name": "Military",
"images": [
{
"id": 1,
"name": "Eagle",
"url": "/content/images/clipart/eagle.svg"
}
]
},
{
"name": "Outdoors",
"images": [...]
}
]
}

Clip art requirements:

  • Format: SVG or high-res PNG
  • Color: Black on transparent background
  • Size: Maximum 500x500px
  • Style: Line art for laser engraving compatibility

Error Handling

Validation

function validateEngravingInput(text, font, size) {
const errors = [];

if (!text || text.trim() === '') {
errors.push('Engraving text is required');
}

if (text.length > 50) {
errors.push('Text must be 50 characters or less');
}

if (!font) {
errors.push('Please select a font');
}

if (!size) {
errors.push('Please select a size');
}

return errors;
}

User Feedback

// Using SweetAlert2
if (errors.length > 0) {
swal.fire({
title: 'Engraving Error',
html: errors.join('<br>'),
icon: 'error'
});
return false;
}

swal.fire({
title: 'Success!',
text: 'Your engraved product has been added to cart',
icon: 'success'
});

API Error Handling

fetch(apiUrl, options)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then(data => {
if (!data.success) {
throw new Error(data.message || 'API request failed');
}
// Process success
})
.catch(error => {
console.error('Engraving API error:', error);
swal.fire({
title: 'Error',
text: 'Unable to process engraving. Please try again.',
icon: 'error'
});
});

Performance Considerations

Canvas Optimization

  • Debounced redraw during drag operations
  • RequestAnimationFrame for smooth animations
  • Canvas size limited to product requirements
  • Image caching for base layers

Image Loading

  • Lazy load clip art gallery
  • Preload font previews
  • Progressive image loading for mockups

API Optimization

  • Batch similar requests
  • Cache font rendering results
  • Compress mockup images before upload

Testing

Browser Compatibility

  • Chrome 90+
  • Firefox 88+
  • Safari 14+
  • Edge 90+

Canvas API support required (no IE11).

Mobile Considerations

  • Touch events for drag-and-drop
  • Responsive canvas sizing
  • Larger hit areas for buttons
  • Simplified UI on small screens

Future Enhancements

Potential improvements:

  • Multiple engraving locations per product
  • Color selection for multi-color engraving
  • Advanced text effects (arc, wave, etc.)
  • Clip art search and filtering
  • Saved designs per customer account
  • 3D preview rendering