{"id":119442,"date":"2026-07-22T10:39:46","date_gmt":"2026-07-22T08:39:46","guid":{"rendered":"https:\/\/svetslik.si\/?p=119442"},"modified":"2026-07-22T10:42:04","modified_gmt":"2026-07-22T08:42:04","slug":"threejs-frame-configurator-woocommerce","status":"publish","type":"post","link":"https:\/\/svetslik.si\/en\/threejs-frame-configurator-woocommerce\/","title":{"rendered":"I put a real-time 3D frame preview on my WooCommerce store. The Three.js was the easy part."},"content":{"rendered":"\n<p>I run <a href=\"https:\/\/svetslik.si\">svetslik.si<\/a>, a small shop in Slovenia selling hand-painted oil paintings and canvas reproductions. Six languages, WooCommerce, shared hosting, no venture money, one developer.<\/p>\n\n<p>We offer 62 frames. For years they were presented the way every art shop presents them: a wall of thumbnails. Customers picked one, received the painting, and sometimes discovered that the frame they imagined and the frame they bought were not the same object. Framing is the highest-margin part of the order and the one people are most reluctant to commit to, because they cannot picture it.<\/p>\n\n<p>So the product page now renders it. Choose a painting, choose a frame, see the combination in the real proportions, in the browser, in real time.<\/p>\n\n<p><strong>Try it: <a href=\"https:\/\/svetslik.si\/en\/gustav-klimt\/the-kiss\/\">https:\/\/svetslik.si\/en\/gustav-klimt\/the-kiss\/<\/a><\/strong> \u2014 open the frame selector and change frames.<\/p>\n\n<video src=\"https:\/\/svetslik.si\/wp-content\/uploads\/2026\/07\/frame-viewer-loop.mp4\" poster=\"https:\/\/svetslik.si\/wp-content\/uploads\/2026\/07\/frame-viewer-poster.jpg\" autoplay loop muted playsinline preload=\"metadata\" style=\"width:100%;height:auto;display:block;margin:2rem 0;\"><\/video>\n\n<p>The Three.js took a weekend. Everything else took months. This is a writeup of everything else, because that is the part nobody writes up.<\/p>\n\n<hr>\n\n<h2>The architecture, and one rule that saved the project<\/h2>\n\n<p>Three pieces:<\/p>\n\n<ol><li><strong>A compiled single-file HTML bundle<\/strong> containing the whole viewer \u2014 2,726 lines.<\/li><li><strong>A bridge mu-plugin<\/strong> in WordPress that mounts it and mediates \u2014 1,486 lines.<\/li><li><strong>A <code>postMessage<\/code> contract<\/strong> between them.<\/li><\/ol>\n\n<p>The viewer lives in an iframe. People push back on this, so let me defend it: <strong>a WooCommerce product page is a hostile environment for a canvas application.<\/strong> Mine carries a page builder that rewrites the DOM, a theme and a child theme, a multilingual plugin, a permalink manager, a caching layer that rewrites markup, and a stack of jQuery plugins that assert themselves on layout whenever they feel like it. Any of them can reach into your canvas container and change its size, its position, or its stacking context.<\/p>\n\n<p>An iframe is a hard boundary. Inside it I control every byte. Outside it I control nothing \u2014 which is fine, because outside it I only need to control six messages:<\/p>\n\n<table>\n<thead><tr><th>Message<\/th><th>Direction<\/th><th>Payload<\/th><\/tr><\/thead>\n<tbody>\n<tr><td><code>svetslik:ready<\/code><\/td><td>child \u2192 parent<\/td><td><code>{ frames: [{ index, key, code, label }, \u2026] }<\/code><\/td><\/tr>\n<tr><td><code>svetslik:aspect<\/code><\/td><td>child \u2192 parent<\/td><td><code>{ ratio }<\/code> \u2014 W\/H<\/td><\/tr>\n<tr><td><code>svetslik:empty<\/code><\/td><td>child \u2192 parent<\/td><td><code>{ empty }<\/code><\/td><\/tr>\n<tr><td><code>svetslik:frame<\/code><\/td><td>parent \u2192 child<\/td><td><code>{ frame }<\/code> \u2014 index, or <code>null<\/code> to clear<\/td><\/tr>\n<tr><td><code>svetslik:painting<\/code><\/td><td>parent \u2192 child<\/td><td><code>{ url }<\/code><\/td><\/tr>\n<tr><td><code>svetslik:dimensions<\/code><\/td><td>parent \u2192 child<\/td><td><code>{ w, h }<\/code> in cm<\/td><\/tr>\n<\/tbody><\/table>\n\n<p>The rule that saved the project: <strong>the compiled bundle is never hand-edited.<\/strong> It is a build output. Every fix goes through source and gets rebuilt. The first time you hand-patch a build artifact to ship a quick fix, you have silently destroyed your ability to rebuild, and you will not find out for three weeks.<\/p>\n\n<hr>\n\n<h2>The sizing handshake: the child reports shape, the parent decides size<\/h2>\n\n<p>This is the problem I most wanted to read about before I started, so it goes first.<\/p>\n\n<p>An iframe that sizes to its content needs to know its content\u2019s height. A full-bleed canvas sizes itself to its container. The container is the iframe. Neither can resolve. Each is waiting for the other.<\/p>\n\n<p>I saw both failure modes. The canvas never getting a size at all, noted in the bundle at the guard that now prevents it:<\/p>\n\n<blockquote><p><em>\u201cit still has to produce (and emit) a height, else the iframe never gets sized and the canvas stays 0 forever (deadlock). Only bail if width is unknown.\u201d<\/em><\/p><\/blockquote>\n\n<p>And the opposite \u2014 a height that resolved to garbage. The product template has its own frame-swatch slider, roughly 870px tall, sitting in the image column. It pushed the iframe down the page until the measured top was 1028px, at which point the available-room calculation went negative and the height collapsed to its 240px floor. A viewer 240 pixels tall is not a viewer.<\/p>\n\n<p>The fix is a rule rather than a trick: <strong>exactly one side owns the size, and it is not the side that knows the content.<\/strong><\/p>\n\n<p>The child never reports pixels. It reports a <em>ratio<\/em>. It projects the frame box\u2019s eight corners across every settle pose, takes the widest and tallest silhouette it will ever occupy, and posts the resulting aspect:<\/p>\n\n<pre><code class=\"language-js\">let worldW = 0, worldH = 0;\nfor (const cs of cornerSets){\n  let xmin=1e9,xmax=-1e9,ymin=1e9,ymax=-1e9;\n  for (const p of cs){ if(p.x<xmin)xmin=p.x; if(p.x>xmax)xmax=p.x; if(p.y<ymin)ymin=p.y; if(p.y>ymax)ymax=p.y; }\n  if (xmax-xmin > worldW) worldW = xmax-xmin;\n  if (ymax-ymin > worldH) worldH = ymax-ymin;\n}\nthis._aspect = Math.max(1e-6, worldW) \/ Math.max(1e-6, worldH);\nthis.emitAspect();<\/code><\/pre>\n\n<p>Using the rotation-safe bounding box rather than the painting\u2019s own dimensions is what stops the iframe from resizing mid-rotation. The parent then owns pixels entirely:<\/p>\n\n<pre><code class=\"language-js\">var width = wrap.clientWidth;\nif ( ! width ) { return; }\nvar ideal = width \/ ( aspect > 0 ? aspect : 0.78 );\nvar cap;\nif ( MOBILE_MQ.matches ) {\n    cap = Math.max( 320, Math.round( window.innerHeight * 0.72 ) );\n} else {\n    var room = window.innerHeight - iframe.getBoundingClientRect().top - galleryRowHeight() - 24;\n    cap = Math.max( 240, room );\n}\niframe.style.height = Math.round( Math.min( ideal, cap ) ) + 'px';<\/code><\/pre>\n\n<p>The other half of the fix is refusing to emit unless the value actually changed:<\/p>\n\n<pre><code class=\"language-js\">emitAspect(){\n  const r = this._aspect; if (r == null || !isFinite(r)) return;\n  if (r === this._lastEmitAspect) return;\n  this._lastEmitAspect = r;\n  try { if (window.parent && window.parent !== window) window.parent.postMessage({ type:'svetslik:aspect', ratio: r }, '*'); } catch(_){}\n}<\/code><\/pre>\n\n<p>Without that guard the frame rotates, the silhouette changes by a rounding error, the parent resizes, the canvas re-lays-out, the silhouette changes again. That is not a bug you find in testing. It is a bug you find on someone else\u2019s laptop.<\/p>\n\n<p>The generalisable version: <strong>any time both sides of an iframe boundary can trigger a resize, you have built a feedback loop.<\/strong> It may be stable today because one side happens to settle first. It will not be stable on a slower device, after a font loads, or when a scrollbar appears. Pick an authority. Let the other side report shape, never size.<\/p>\n\n<hr>\n\n<h2>Two materials, two philosophies<\/h2>\n\n<p>The first renders looked expensive and wrong. Beautifully lit, convincing depth, and a gold that was not the gold of the painting.<\/p>\n\n<p>The renderer is doing real work \u2014 sRGB output, ACES filmic tone mapping, exposure at 1.15:<\/p>\n\n<pre><code class=\"language-js\">const renderer = new THREE.WebGLRenderer({ canvas: cv, antialias: true, alpha: true });\nrenderer.outputColorSpace = THREE.SRGBColorSpace;\nrenderer.toneMapping = THREE.ACESFilmicToneMapping;\nrenderer.toneMappingExposure = 1.15;<\/code><\/pre>\n\n<p>That is correct for the moulding. A picture frame is a physical object with gilding and relief, and it should be lit, shaded and tone-mapped like one.<\/p>\n\n<p>It is completely wrong for the artwork. The painting face is not an object in the scene \u2014 it is a photograph of a thing the customer is about to compare against another photograph of the same thing. Run it through a lighting rig and a filmic curve and bright paintings wash out. So the painting opts out of the entire pipeline:<\/p>\n\n<pre><code class=\"language-js\">const pmat = new THREE.MeshBasicMaterial({ toneMapped: false, color: 0xcfc3a8 });<\/code><\/pre>\n\n<p>Unlit material, tone mapping explicitly off, and when the real texture arrives it is declared sRGB and the base tint is cleared to white so nothing tints it:<\/p>\n\n<pre><code class=\"language-js\">tex.colorSpace = THREE.SRGBColorSpace;\n\/\/ on load:\npmat.map = tex; pmat.color.setHex(0xffffff);<\/code><\/pre>\n\n<p><strong>The frame is simulated. The artwork is reproduced.<\/strong> Two materials, two philosophies, one scene.<\/p>\n\n<p>This is the opposite of what a Three.js tutorial teaches, and it is the right call whenever the customer will compare your render against a photograph. Nobody has ever asked for a more convincing specular highlight on a picture frame. They ask whether it is the same gold.<\/p>\n\n<p>The <code>0xcfc3a8<\/code> starting colour is deliberate too \u2014 a neutral raw-canvas tone, so that in the moment before the texture loads you see something that looks like an unpainted canvas rather than a white flash.<\/p>\n\n<hr>\n\n<h2>Paying the boot cost when nobody is looking<\/h2>\n\n<p>Profiling on a real GPU put viewer boot at <strong>~4.1s cold, ~1.2s warm, with about 72% of that being synchronous shader compilation<\/strong> paid once per boot.<\/p>\n\n<p>Four seconds of frozen main thread is not an acceptable response to a tap. But notice the shape of that number: it is a one-off. So the fix is not to make it faster, it is to spend it earlier, while the customer is doing something else.<\/p>\n\n<p>The parent watches for the first interaction with the frame selector, in the capture phase, before the dropdown has even finished opening:<\/p>\n\n<pre><code class=\"language-js\">function prebootViewer( e ) {\n    if ( viewerBuilt ) { removePrebootListeners(); return; }\n    var t = e.target;\n    if ( ! t || ! t.closest || ! t.closest( '.sds--okvir, .sds--podokvir' ) ) {\n        return;                          \/\/ some other control \u2014 keep waiting\n    }\n    if ( buildViewer() ) {\n        removePrebootListeners();\n    }\n}\ndocument.addEventListener( 'pointerdown', prebootViewer, true );\ndocument.addEventListener( 'keydown', prebootViewer, true );<\/code><\/pre>\n\n<p><code>buildViewer()<\/code> creates the iframe and sets its <code>src<\/code>, but the wrapper is <code>display:none<\/code> and never gets its <code>is-on<\/code> class. Three.js parses, the WebGL context is created, and every shader compiles \u2014 entirely invisibly, while the customer is still reading a list of frame names. The comment in the bridge puts it plainly: the one-off boot cost is paid before the eventual pick.<\/p>\n\n<p>By the time they choose a frame, the cost is already sunk and the reveal is instant. Nothing was optimised. It was just moved.<\/p>\n\n<hr>\n\n<h2>The failure that has no error message<\/h2>\n\n<p>There is no WebGL feature detection anywhere in this codebase. I want to explain why, because I originally considered that a gap and now consider it the design.<\/p>\n\n<p>Detection is failure-based. <code>init()<\/code> tries, and readiness is signalled by a message that only ever gets posted if everything worked:<\/p>\n\n<pre><code class=\"language-js\">init(){\n  try {\n    const THREE = window.THREE;\n    if (!THREE) { console.error('three.js failed to load'); return; }\n    this.THREE = THREE;\n    this.initScene();                 \/\/ new THREE.WebGLRenderer(...) \u2014 throws with no WebGL\n    this.setupMessaging();            \/\/ posts svetslik:ready \u2014 only reached if initScene succeeded\n    this.buildFrame(this.selected);\n    this.startLoop();\n    this.initDrag();\n    this.loadingEl.style.display = 'none';\n    this.reveal();\n  } catch(e){ console.error('three init failed', e); }\n}<\/code><\/pre>\n\n<p>The ordering is the whole mechanism. <code>setupMessaging()<\/code> runs <em>after<\/em> <code>initScene()<\/code>, so if the WebGL context throws, <code>svetslik:ready<\/code> is never posted. The parent draws its conclusion from silence:<\/p>\n\n<pre><code class=\"language-js\">var READY_WATCHDOG_MS = 10000;\n\/\/ armed when the iframe is created:\nreadyWatchdog = setTimeout( function () {\n    readyWatchdog = null;\n    if ( ! viewerReady ) {\n        viewerDead = true;\n        hideViewer();\n        update2dFallback();\n    }\n}, READY_WATCHDOG_MS );<\/code><\/pre>\n\n<p>A feature probe answers exactly one question: does this browser have WebGL. The watchdog answers the question I actually care about: <strong>is this customer going to see a working 3D preview in a reasonable amount of time.<\/strong> No WebGL, a bundle that failed to download, an exception in my own code, a device too slow to boot inside ten seconds \u2014 one mechanism catches all of it, and the customer gets the normal product gallery either way.<\/p>\n\n<p>One refinement that matters. A late <code>ready<\/code> revives a dead session rather than being ignored \u2014 the note in the source reads <em>\u201clate ready = slow device, not crippled.\u201d<\/em> A slow phone that boots in twelve seconds still gets the viewer; it just arrives after the fallback did.<\/p>\n\n<p>And if you are wondering who actually has no WebGL in 2026, the reproduction case was not an ancient Android. It was <strong>in-app browsers \u2014 Facebook, Instagram, Messenger.<\/strong> A large share of e-commerce traffic arrives through exactly those WebViews. If you ship WebGL to a store, that is your fallback path, and you will never see it in your own testing because you do not browse your own shop through a Facebook link.<\/p>\n\n<hr>\n\n<h2>Mobile: a different mount, not a different stylesheet<\/h2>\n\n<p>The first mobile version replaced the product image with the viewer. Small screen, one hero slot, put the impressive thing in it. It was wrong.<\/p>\n\n<p><strong>The painting is the product. The frame is an accessory.<\/strong> Replacing the photograph with a 3D toy removed the thing that makes someone want to buy and substituted the thing that makes them want to play. Those are different behaviours and only one ends in a purchase.<\/p>\n\n<p>The fix is not CSS. Below 991px the viewer gets a genuinely different DOM parent \u2014 a purpose-built div injected directly after the variations table, whose last visible row is the frame selector:<\/p>\n\n<pre><code class=\"language-js\">var sel = document.getElementById( 'pa_okvir_select' );\nif ( ! sel || 'SELECT' !== sel.tagName ) { return null; }\nvar table = sel.closest( 'table' );\nif ( ! table || ! table.parentNode ) { return null; }\nmobileMount = document.createElement( 'div' );\nmobileMount.id = 'svetslik-3d-mobile-mount';\ntable.parentNode.insertBefore( mobileMount, table.nextSibling );\nreturn mobileMount;<\/code><\/pre>\n\n<pre><code class=\"language-js\">function currentMount() {\n    return ( MOBILE_MQ.matches && getMobileMount() ) || host;\n}<\/code><\/pre>\n\n<p>On desktop the mount is the image column. On mobile the reading order becomes frame selector \u2192 viewer \u2192 swatch gallery \u2192 moulding close-up, and the product photograph stays exactly where it was, untouched. The configurator appears at the decision point instead of replacing the thing that created the desire.<\/p>\n\n<p>One cost worth knowing: crossing the breakpoint reparents the iframe, and reparenting an iframe reloads its document. Readiness is dropped and the watchdog re-armed. Rotating your phone re-boots the viewer. I accepted that; the alternative is two live WebGL contexts on a phone.<\/p>\n\n<p><strong>A configurator is a decision aid, not the product.<\/strong> It belongs where the decision happens.<\/p>\n\n<hr>\n\n<h2>The bug that threw no error<\/h2>\n\n<p>Worth its own section because of how it presented.<\/p>\n\n<p>Early on the viewer simply did nothing. No console error, no failed request, no exception. Frames were selected and nothing happened.<\/p>\n\n<p>The bridge was sending <code>index:<\/code> and <code>src:<\/code>. The viewer was reading <code>frame:<\/code> and <code>url:<\/code>. Both sides worked perfectly. The condition just never passed.<\/p>\n\n<p><code>postMessage<\/code> has no schema, no type checking, and no failure mode for a message nobody wanted. If you build a contract across that boundary, write it down in one place \u2014 I keep it as a comment block in the bundle \u2014 and make one side the canonical definition. A silent no-op is a considerably worse debugging experience than a thrown error, and this is a boundary that produces them by default.<\/p>\n\n<hr>\n\n<h2>The unglamorous 80%: the catalogue<\/h2>\n\n<p>The rendering is the part that photographs well. Here is the part that consumed the schedule.<\/p>\n\n<p>62 physical frames had to be catalogued, measured, profiled and turned into assets, producing 22 new WooCommerce products and a set of moulding textures I ended up rendering with Three.js itself \u2014 the engine that displays the frames also generated the source images for them.<\/p>\n\n<p>Then the commerce plumbing, where the interesting failures live. Painting dimensions arrive as attribute slugs, and slug conventions accumulate history. The parser handled <code>{W}-cm-x-{H}-cm<\/code> and <code>{W}-x-{H}-cm<\/code>. It did not handle the legacy bare <code>N-x-N<\/code> form, used by 17 terms. Those returned <code>null<\/code>, the dimensions message was never sent, and the viewer silently kept its default 40\u00d730 opening \u2014 so Monet\u2019s water lilies at 260\u00d760, a 4.33:1 panorama, rendered approximately square.<\/p>\n\n<pre><code class=\"language-js\">var m = slug.match( new RegExp( '^' + NUM + '-cm-x-' + NUM + '-cm$' ) )\n    || slug.match( new RegExp( '^' + NUM + '-x-' + NUM + '-cm$' ) )\n    || slug.match( \/^(\\d+)-x-(\\d+)$\/ );   \/\/ legacy bare format<\/code><\/pre>\n\n<p>One added alternation. Days to find, because the failure was a plausible-looking render rather than an error.<\/p>\n\n<p>None of this is interesting. All of it is the project. If you are estimating a configurator build, estimate the catalogue and the commerce integration, then add a weekend for the 3D.<\/p>\n\n<hr>\n\n<h2>What I would tell you before you start<\/h2>\n\n<ol><li><strong>Put it in an iframe.<\/strong> A commerce product page is not somewhere you control the DOM.<\/li><li><strong>One side owns the size.<\/strong> Let the content report <em>shape<\/em>; let the host decide <em>pixels<\/em>.<\/li><li><strong>Emit only on change.<\/strong> An aspect ratio posted every animation frame is a resize loop.<\/li><li><strong>Simulate the product\u2019s container, reproduce the product.<\/strong> Tone-map the frame, never the artwork.<\/li><li><strong>Move the boot cost, don\u2019t optimise it.<\/strong> 4.1 seconds nobody is waiting through is free.<\/li><li><strong>Prefer a readiness watchdog to a feature probe.<\/strong> It catches every reason the thing didn\u2019t appear, not one.<\/li><li><strong>Your no-WebGL users are in Facebook\u2019s browser,<\/strong> not on a museum-piece Android.<\/li><li><strong>On mobile, add the configurator \u2014 never substitute it for the product image.<\/strong><\/li><li><strong>Write the postMessage contract down.<\/strong> Nothing on that boundary will ever tell you it is wrong.<\/li><li><strong>The 3D is a weekend. The catalogue is the project.<\/strong><\/li><\/ol>\n\n<p>It is live: <strong><a href=\"https:\/\/svetslik.si\/en\/gustav-klimt\/the-kiss\/\">https:\/\/svetslik.si\/en\/gustav-klimt\/the-kiss\/<\/a><\/strong> \u2014 pick a frame and drag to rotate.<\/p>\n\n","protected":false},"excerpt":{"rendered":"<p>I run svetslik.si, a small shop in Slovenia selling hand-painted oil paintings and canvas reproductions. Six languages, WooCommerce, shared hosting, no venture money, one developer. We offer 62 frames. For years they were presented the way every art shop presents them: a wall of thumbnails. Customers picked one, received the [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[],"tags":[],"class_list":["post-119442","post","type-post","status-publish","format-standard","hentry"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v27.7 (Yoast SEO v27.7) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>A real-time 3D frame configurator built on WooCommerce<\/title>\n<meta name=\"description\" content=\"How I built a real-time 3D picture-frame preview for a WooCommerce store with Three.js: the iframe sizing handshake, colour, and WebGL fallback.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/svetslik.si\/en\/threejs-frame-configurator-woocommerce\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"I put a real-time 3D frame preview on my WooCommerce store. The Three.js was the easy part. - SvetSlik.si\" \/>\n<meta property=\"og:description\" content=\"100% hand-painted reproductions of famous painters, abstract paintings, wall art on canvas, modern paintings, and custom paintings.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/svetslik.si\/en\/threejs-frame-configurator-woocommerce\/\" \/>\n<meta property=\"og:site_name\" content=\"SvetSlik.si\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/svetslik.si\" \/>\n<meta property=\"article:published_time\" content=\"2026-07-22T08:39:46+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-07-22T08:42:04+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/svetslik.si\/wp-content\/uploads\/2023\/01\/svetslik.si_.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1736\" \/>\n\t<meta property=\"og:image:height\" content=\"570\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"svetslik\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@svetslik\" \/>\n<meta name=\"twitter:site\" content=\"@svetslik\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"svetslik\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"13 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":[\"Article\",\"BlogPosting\"],\"@id\":\"https:\\\/\\\/svetslik.si\\\/en\\\/threejs-frame-configurator-woocommerce\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/svetslik.si\\\/en\\\/threejs-frame-configurator-woocommerce\\\/\"},\"author\":{\"name\":\"svetslik\",\"@id\":\"https:\\\/\\\/svetslik.si\\\/en\\\/#\\\/schema\\\/person\\\/b88e7dc1d7b5a07806cd25804c14fa87\"},\"headline\":\"I put a real-time 3D frame preview on my WooCommerce store. The Three.js was the easy part.\",\"datePublished\":\"2026-07-22T08:39:46+00:00\",\"dateModified\":\"2026-07-22T08:42:04+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/svetslik.si\\\/en\\\/threejs-frame-configurator-woocommerce\\\/\"},\"wordCount\":2184,\"publisher\":{\"@id\":\"https:\\\/\\\/svetslik.si\\\/en\\\/#organization\"},\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/svetslik.si\\\/en\\\/threejs-frame-configurator-woocommerce\\\/\",\"url\":\"https:\\\/\\\/svetslik.si\\\/en\\\/threejs-frame-configurator-woocommerce\\\/\",\"name\":\"A real-time 3D frame configurator built on WooCommerce\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/svetslik.si\\\/en\\\/#website\"},\"datePublished\":\"2026-07-22T08:39:46+00:00\",\"dateModified\":\"2026-07-22T08:42:04+00:00\",\"description\":\"How I built a real-time 3D picture-frame preview for a WooCommerce store with Three.js: the iframe sizing handshake, colour, and WebGL fallback.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/svetslik.si\\\/en\\\/threejs-frame-configurator-woocommerce\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/svetslik.si\\\/en\\\/threejs-frame-configurator-woocommerce\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/svetslik.si\\\/en\\\/threejs-frame-configurator-woocommerce\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Paintings on canvas\",\"item\":\"https:\\\/\\\/svetslik.si\\\/en\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"I put a real-time 3D frame preview on my WooCommerce store. The Three.js was the easy part.\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/svetslik.si\\\/en\\\/#website\",\"url\":\"https:\\\/\\\/svetslik.si\\\/en\\\/\",\"name\":\"SvetSlik.si\",\"description\":\"100% hand-painted reproductions of famous painters, abstract paintings, wall art on canvas, modern paintings, and custom paintings.\",\"publisher\":{\"@id\":\"https:\\\/\\\/svetslik.si\\\/en\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/svetslik.si\\\/en\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Organization\",\"Store\"],\"@id\":\"https:\\\/\\\/svetslik.si\\\/en\\\/#organization\",\"name\":\"SvetSlik.si\",\"url\":\"https:\\\/\\\/svetslik.si\\\/en\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/svetslik.si\\\/en\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/svetslik.si\\\/wp-content\\\/uploads\\\/2020\\\/12\\\/logo-512.png\",\"contentUrl\":\"https:\\\/\\\/svetslik.si\\\/wp-content\\\/uploads\\\/2020\\\/12\\\/logo-512.png\",\"width\":512,\"height\":512,\"caption\":\"SvetSlik.si\"},\"image\":{\"@id\":\"https:\\\/\\\/svetslik.si\\\/en\\\/#\\\/schema\\\/logo\\\/image\\\/\"},\"sameAs\":[\"https:\\\/\\\/www.facebook.com\\\/svetslik.si\",\"https:\\\/\\\/x.com\\\/svetslik\",\"https:\\\/\\\/www.instagram.com\\\/svetslik.si\\\/\",\"https:\\\/\\\/www.linkedin.com\\\/company\\\/svetslik-si\",\"https:\\\/\\\/www.google.com\\\/maps\\\/place\\\/?q=place_id:ChIJaybyg-XHekcRwZCqYrxOGjY\"],\"address\":{\"@type\":\"PostalAddress\",\"streetAddress\":\"Zgornje Bitnje 146a\",\"addressLocality\":\"\u017dabnica\",\"postalCode\":\"4209\",\"addressCountry\":\"SI\"},\"hasMap\":\"https:\\\/\\\/www.google.com\\\/maps\\\/place\\\/?q=place_id:ChIJaybyg-XHekcRwZCqYrxOGjY\",\"telephone\":\"+38651675637\",\"openingHoursSpecification\":[{\"@type\":\"OpeningHoursSpecification\",\"dayOfWeek\":[\"https:\\\/\\\/schema.org\\\/Monday\",\"https:\\\/\\\/schema.org\\\/Tuesday\",\"https:\\\/\\\/schema.org\\\/Wednesday\",\"https:\\\/\\\/schema.org\\\/Thursday\",\"https:\\\/\\\/schema.org\\\/Friday\"],\"opens\":\"09:00\",\"closes\":\"20:00\"}],\"description\":\"Hand-painted art on canvas \u2013 order online or by email. Viewing available by prior arrangement.\",\"knowsAbout\":[\"canvas paintings\",\"hand-painted canvas art\",\"art reproductions\",\"oil on canvas\",\"acrylic on canvas\",\"modern abstract art\",\"commissioned painting\",\"made-to-order paintings\",\"picture framing\",\"paintings by famous artists\",\"modern art for home\",\"painting as a gift\",\"custom canvas art\",\"art delivery\",\"art gallery\"],\"legalName\":\"Matemero d.o.o.\",\"email\":\"info@svetslik.si\",\"foundingDate\":\"2009\",\"aggregateRating\":{\"@type\":\"AggregateRating\",\"ratingValue\":\"5\",\"reviewCount\":97,\"bestRating\":\"5\",\"worstRating\":\"1\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/svetslik.si\\\/en\\\/#\\\/schema\\\/person\\\/b88e7dc1d7b5a07806cd25804c14fa87\",\"name\":\"svetslik\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/svetslik.si\\\/wp-content\\\/uploads\\\/nsl_avatars\\\/d23d0eae6d69da2b5d876c86b0b491b8.png\",\"url\":\"https:\\\/\\\/svetslik.si\\\/wp-content\\\/uploads\\\/nsl_avatars\\\/d23d0eae6d69da2b5d876c86b0b491b8.png\",\"contentUrl\":\"https:\\\/\\\/svetslik.si\\\/wp-content\\\/uploads\\\/nsl_avatars\\\/d23d0eae6d69da2b5d876c86b0b491b8.png\",\"caption\":\"svetslik\"},\"sameAs\":[\"http:\\\/\\\/localhost\\\/svetslik\"],\"url\":\"https:\\\/\\\/svetslik.si\\\/en\\\/author\\\/svetslik\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"A real-time 3D frame configurator built on WooCommerce","description":"How I built a real-time 3D picture-frame preview for a WooCommerce store with Three.js: the iframe sizing handshake, colour, and WebGL fallback.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/svetslik.si\/en\/threejs-frame-configurator-woocommerce\/","og_locale":"en_US","og_type":"article","og_title":"I put a real-time 3D frame preview on my WooCommerce store. The Three.js was the easy part. - SvetSlik.si","og_description":"100% hand-painted reproductions of famous painters, abstract paintings, wall art on canvas, modern paintings, and custom paintings.","og_url":"https:\/\/svetslik.si\/en\/threejs-frame-configurator-woocommerce\/","og_site_name":"SvetSlik.si","article_publisher":"https:\/\/www.facebook.com\/svetslik.si","article_published_time":"2026-07-22T08:39:46+00:00","article_modified_time":"2026-07-22T08:42:04+00:00","og_image":[{"width":1736,"height":570,"url":"https:\/\/svetslik.si\/wp-content\/uploads\/2023\/01\/svetslik.si_.jpg","type":"image\/jpeg"}],"author":"svetslik","twitter_card":"summary_large_image","twitter_creator":"@svetslik","twitter_site":"@svetslik","twitter_misc":{"Written by":"svetslik","Est. reading time":"13 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":["Article","BlogPosting"],"@id":"https:\/\/svetslik.si\/en\/threejs-frame-configurator-woocommerce\/#article","isPartOf":{"@id":"https:\/\/svetslik.si\/en\/threejs-frame-configurator-woocommerce\/"},"author":{"name":"svetslik","@id":"https:\/\/svetslik.si\/en\/#\/schema\/person\/b88e7dc1d7b5a07806cd25804c14fa87"},"headline":"I put a real-time 3D frame preview on my WooCommerce store. The Three.js was the easy part.","datePublished":"2026-07-22T08:39:46+00:00","dateModified":"2026-07-22T08:42:04+00:00","mainEntityOfPage":{"@id":"https:\/\/svetslik.si\/en\/threejs-frame-configurator-woocommerce\/"},"wordCount":2184,"publisher":{"@id":"https:\/\/svetslik.si\/en\/#organization"},"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/svetslik.si\/en\/threejs-frame-configurator-woocommerce\/","url":"https:\/\/svetslik.si\/en\/threejs-frame-configurator-woocommerce\/","name":"A real-time 3D frame configurator built on WooCommerce","isPartOf":{"@id":"https:\/\/svetslik.si\/en\/#website"},"datePublished":"2026-07-22T08:39:46+00:00","dateModified":"2026-07-22T08:42:04+00:00","description":"How I built a real-time 3D picture-frame preview for a WooCommerce store with Three.js: the iframe sizing handshake, colour, and WebGL fallback.","breadcrumb":{"@id":"https:\/\/svetslik.si\/en\/threejs-frame-configurator-woocommerce\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/svetslik.si\/en\/threejs-frame-configurator-woocommerce\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/svetslik.si\/en\/threejs-frame-configurator-woocommerce\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Paintings on canvas","item":"https:\/\/svetslik.si\/en\/"},{"@type":"ListItem","position":2,"name":"I put a real-time 3D frame preview on my WooCommerce store. The Three.js was the easy part."}]},{"@type":"WebSite","@id":"https:\/\/svetslik.si\/en\/#website","url":"https:\/\/svetslik.si\/en\/","name":"SvetSlik.si","description":"100% hand-painted reproductions of famous painters, abstract paintings, wall art on canvas, modern paintings, and custom paintings.","publisher":{"@id":"https:\/\/svetslik.si\/en\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/svetslik.si\/en\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Organization","Store"],"@id":"https:\/\/svetslik.si\/en\/#organization","name":"SvetSlik.si","url":"https:\/\/svetslik.si\/en\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/svetslik.si\/en\/#\/schema\/logo\/image\/","url":"https:\/\/svetslik.si\/wp-content\/uploads\/2020\/12\/logo-512.png","contentUrl":"https:\/\/svetslik.si\/wp-content\/uploads\/2020\/12\/logo-512.png","width":512,"height":512,"caption":"SvetSlik.si"},"image":{"@id":"https:\/\/svetslik.si\/en\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/svetslik.si","https:\/\/x.com\/svetslik","https:\/\/www.instagram.com\/svetslik.si\/","https:\/\/www.linkedin.com\/company\/svetslik-si","https:\/\/www.google.com\/maps\/place\/?q=place_id:ChIJaybyg-XHekcRwZCqYrxOGjY"],"address":{"@type":"PostalAddress","streetAddress":"Zgornje Bitnje 146a","addressLocality":"\u017dabnica","postalCode":"4209","addressCountry":"SI"},"hasMap":"https:\/\/www.google.com\/maps\/place\/?q=place_id:ChIJaybyg-XHekcRwZCqYrxOGjY","telephone":"+38651675637","openingHoursSpecification":[{"@type":"OpeningHoursSpecification","dayOfWeek":["https:\/\/schema.org\/Monday","https:\/\/schema.org\/Tuesday","https:\/\/schema.org\/Wednesday","https:\/\/schema.org\/Thursday","https:\/\/schema.org\/Friday"],"opens":"09:00","closes":"20:00"}],"description":"Hand-painted art on canvas \u2013 order online or by email. Viewing available by prior arrangement.","knowsAbout":["canvas paintings","hand-painted canvas art","art reproductions","oil on canvas","acrylic on canvas","modern abstract art","commissioned painting","made-to-order paintings","picture framing","paintings by famous artists","modern art for home","painting as a gift","custom canvas art","art delivery","art gallery"],"legalName":"Matemero d.o.o.","email":"info@svetslik.si","foundingDate":"2009","aggregateRating":{"@type":"AggregateRating","ratingValue":"5","reviewCount":97,"bestRating":"5","worstRating":"1"}},{"@type":"Person","@id":"https:\/\/svetslik.si\/en\/#\/schema\/person\/b88e7dc1d7b5a07806cd25804c14fa87","name":"svetslik","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/svetslik.si\/wp-content\/uploads\/nsl_avatars\/d23d0eae6d69da2b5d876c86b0b491b8.png","url":"https:\/\/svetslik.si\/wp-content\/uploads\/nsl_avatars\/d23d0eae6d69da2b5d876c86b0b491b8.png","contentUrl":"https:\/\/svetslik.si\/wp-content\/uploads\/nsl_avatars\/d23d0eae6d69da2b5d876c86b0b491b8.png","caption":"svetslik"},"sameAs":["http:\/\/localhost\/svetslik"],"url":"https:\/\/svetslik.si\/en\/author\/svetslik\/"}]}},"_links":{"self":[{"href":"https:\/\/svetslik.si\/en\/wp-json\/wp\/v2\/posts\/119442","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/svetslik.si\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/svetslik.si\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/svetslik.si\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/svetslik.si\/en\/wp-json\/wp\/v2\/comments?post=119442"}],"version-history":[{"count":0,"href":"https:\/\/svetslik.si\/en\/wp-json\/wp\/v2\/posts\/119442\/revisions"}],"wp:attachment":[{"href":"https:\/\/svetslik.si\/en\/wp-json\/wp\/v2\/media?parent=119442"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/svetslik.si\/en\/wp-json\/wp\/v2\/categories?post=119442"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/svetslik.si\/en\/wp-json\/wp\/v2\/tags?post=119442"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}