There is no product photograph anywhere on this page. The lamp is geometry generated in the browser at runtime, and the finish you pick drives two things at once: the material on the model, and the Shopify variant that goes into the cart. Built as a Shopify section, so a merchant edits it from the theme customiser like any other.
No assets to break
The shade, stem and base are lathe geometry — a profile revolved in code. Nothing is downloaded, so there is no model file to go missing, no 40 MB payload, and no licence attached to a mesh someone else made.
One source of truth
Finishes are read out of the Shopify product, not hard-coded. Add a fourth variant in the admin and a fourth swatch appears here, wired to the right price and the right material.
Degrades honestly
If WebGL is unavailable the section falls back to the product's normal image gallery rather than showing an empty box. Reduced-motion users get the model without the idle drift.
sections/product-3d.liquidShopify · Liquid
{%- comment -%} Variant data is emitted from the product, never hard-coded {%- endcomment -%}
<script type="application/json" data-solen-variants>
[
{%- for v in product.variants -%}
{
"id": {{ v.id }},
"title": {{ v.title | json }},
"price": {{ v.price | money | json }},
"available": {{ v.available }},
"finish": {{ v.title | handleize | json }}
}{%- unless forloop.last -%},{%- endunless -%}
{%- endfor -%}
]
</script>
{%- comment -%} Swatches are rendered from the option, so a new variant needs no code change {%- endcomment -%}
<div class="swatches" role="radiogroup">
{%- for value in product.options_by_name['Finish'].values -%}
<button role="radio"
aria-checked="{% if forloop.first %}true{% else %}false{% endif %}"
data-finish="{{ value | handleize }}">{{ value }}</button>
{%- endfor -%}
</div>
{%- comment -%} Real cart form. The 3D layer only ever changes which variant id is selected. {%- endcomment -%}
{%- form 'product', product -%}
<input type="hidden" name="id" data-variant-id
value="{{ product.selected_or_first_available_variant.id }}">
<button type="submit" class="cart">
{{ 'products.add_to_cart' | t }} · <span data-price>
{{ product.selected_or_first_available_variant.price | money }}</span>
</button>
{%- endform -%}