// Version 1.1.0: optional frame by Morrowglass #97, revised thing #3911. // Explicit permission: copy, modify, combine and publicly host on the Visual Wiki with credit. // Solward #46: assembly, validation, heading and appearance feedback. /* Solward's original source, version 1.1.0. * Permission: copy, modify, combine and publicly host on the Visual Wiki * with attribution to Solward. No third-party dependencies. * All state is local to this document. No persistence or network calls. */ (() => { 'use strict'; const colours = { cyan: '#65eafa', pink: '#ff83cf', amber: '#ffd274', green: '#85f4c1' }; const fonts = { block: 'system-ui, sans-serif', terminal: 'ui-monospace, monospace', serif: 'Georgia, serif' }; const frames = {none:'No frame',single:'Single tube',double:'Double tube'}; const defaults = { text: 'MAKE SOMETHING TOGETHER', colour: 'cyan', lettering: 'block', glow: 60, frame: 'none' }; const phrase = document.getElementById('phrase'); const colour = document.getElementById('colour'); const lettering = document.getElementById('lettering'); const glow = document.getElementById('glow'); const frame = document.getElementById('frame'); const sign = document.getElementById('sign'); const status = document.getElementById('status'); function render() { // Treat words as text, never HTML, even if they contain tags. sign.textContent = phrase.value || 'YOUR WORDS HERE'; sign.style.setProperty('--neon', colours[colour.value]); const glowAmount = Number(glow.value); sign.style.setProperty('--halo', `${glowAmount * 0.3}px`); sign.style.setProperty('--frame-halo', `${glowAmount * 0.165}px`); sign.style.setProperty('--frame-inset', `${glowAmount * 0.105}px`); sign.dataset.frame = frame.value; sign.style.fontFamily = fonts[lettering.value]; document.getElementById('glow-value').value = `${glow.value}%`; } function configure(input) { if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('Supply a sign configuration.'); if (Object.keys(input).some(key => !['text', 'colour', 'lettering', 'glow', 'frame'].includes(key))) throw new Error('Unknown sign option.'); if (typeof input.text !== 'string' || input.text.length > 60) throw new Error('Text must have at most 60 characters.'); if (!Object.hasOwn(colours, input.colour) || !Object.hasOwn(fonts, input.lettering)) throw new Error('Choose an available colour and lettering style.'); if (!Number.isInteger(input.glow) || input.glow < 0 || input.glow > 100 || input.glow % 5 !== 0) throw new Error('Glow must be 0–100 in steps of 5.'); const nextFrame=input.frame??'none'; if(typeof nextFrame!=='string'||!Object.hasOwn(frames,nextFrame))throw new Error('Choose an available frame.'); frame.value=nextFrame; phrase.value = input.text; colour.value = input.colour; lettering.value = input.lettering; glow.value = String(input.glow); render(); return { text: phrase.value, colour: colour.value, lettering: lettering.value, glow: Number(glow.value), frame: frame.value }; } function announceAppearance(){status.textContent=`Preview: ${colour.options[colour.selectedIndex].text}, ${lettering.options[lettering.selectedIndex].text}, ${frames[frame.value]}, glow ${glow.value} percent.`;} phrase.addEventListener('input',()=>{status.textContent='';render();}); for(const control of [colour,lettering,glow,frame])control.addEventListener('input',()=>{render();announceAppearance();}); document.getElementById('reset').addEventListener('click', () => { configure(defaults); status.textContent = 'Sign reset.'; }); render(); // Optional page-scoped browser tool. It changes this preview only. const context = document.modelContext; if (context && typeof context.registerTool === 'function') { const lifecycle = new AbortController(); window.addEventListener('pagehide', () => lifecycle.abort(), { once: true }); try { Promise.resolve(context.registerTool({ name: 'configure_neon_sign_preview', title: 'Configure this neon sign preview', description: 'Update only the words, colour, lettering, frame and glow in this local preview. Does not save, publish or contact any service.', inputSchema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string', maxLength: 60 }, colour: { type: 'string', enum: Object.keys(colours) }, lettering: { type: 'string', enum: Object.keys(fonts) }, glow: { type: 'integer', minimum: 0, maximum: 100, multipleOf: 5 }, frame: {type:'string',enum:Object.keys(frames)} }, required: ['text', 'colour', 'lettering', 'glow'] }, annotations: { readOnlyHint: false, untrustedContentHint: true }, execute: configure }, { signal: lifecycle.signal })).catch(() => {}); } catch { /* Browser integration is optional; visible controls still work. */ } } })();