diff --git a/app.js b/app.js index c8563bc..16d8560 100644 --- a/app.js +++ b/app.js @@ -20,7 +20,34 @@ function App() { "l": "bold", "m": "strikethrough", "n": "underline", - "o": "italic" + "o": "italic", + "g": "minecoin_gold", + "h": "material_quartz", + "i": "material_iron", + "j": "material_netherite", + "p": "material_gold", + "q": "material_emerald", + "s": "material_diamond", + "t": "material_lapis", + "u": "material_amethyst", + "v": "material_resin", + "w": "party_blue" + } + + const BEDROCK_FCODES = { + "g": "minecoin_gold", + "h": "material_quartz", + "i": "material_iron", + "j": "material_netherite", + "m": "material_redstone", + "n": "material_copper", + "p": "material_gold", + "q": "material_emerald", + "s": "material_diamond", + "t": "material_lapis", + "u": "material_amethyst", + "v": "material_resin", + "w": "party_blue" } const NAMES_TO_FCODES = { @@ -44,11 +71,95 @@ function App() { "bold": "l", "strikethrough": "m", "underline": "n", - "italic": "o" + "italic": "o", + "minecoin_gold": "g", + "material_quartz": "h", + "material_iron": "i", + "material_netherite": "j", + "material_redstone": "m", + "material_copper": "n", + "material_gold": "p", + "material_emerald": "q", + "material_diamond": "s", + "material_lapis": "t", + "material_amethyst": "u", + "material_resin": "v", + "party_blue": "w" + } + + const NAMED_COLORS = { + black: 0x000000, + dark_blue: 0x0000aa, + dark_green: 0x00aa00, + dark_aqua: 0x00aaaa, + dark_red: 0xaa0000, + dark_purple: 0xaa00aa, + gold: 0xffaa00, + orange: 0xffaa00, + gray: 0xaaaaaa, + grey: 0xaaaaaa, + dark_gray: 0x555555, + dark_grey: 0x555555, + blue: 0x5555ff, + green: 0x55ff55, + aqua: 0x55ffff, + red: 0xff5555, + pink: 0xff55ff, + light_purple: 0xff55ff, + yellow: 0xffff55, + white: 0xffffff, + minecoin_gold: 0xddd605, + material_quartz: 0xe3d4d1, + material_iron: 0xcecaca, + material_netherite: 0x443a3b, + material_redstone: 0x971607, + material_copper: 0xb4684d, + material_gold: 0xdeb12d, + material_emerald: 0x119f36, + material_diamond: 0x2cbaa8, + material_lapis: 0x21497b, + material_amethyst: 0x9a5cc6, + material_resin: 0xeb7114, + party_blue: 0x8cb3ff } let text_json_record = [] + function isBedrockEdition() { + return document.querySelector('#edition')?.value === 'bedrock' + } + + function isLegacyColorCode(code) { + const lower = code.toLowerCase() + if (/[0-9a-f]/.test(lower)) return true + if (/[ghijpqrstuvw]/.test(lower)) return true + if (isBedrockEdition() && /[mn]/.test(lower)) return true + return false + } + + function isLegacyFormatCode(code) { + const lower = code.toLowerCase() + if (isBedrockEdition()) return /[klo]/.test(lower) + return /[klmno]/.test(lower) + } + + function legacySplitPattern() { + return /([&§][0-9a-fg-w](?:(?![&§][0-9a-fg-w])[\s\S])*)/gi + } + + function legacyColorName(code) { + const lower = code.toLowerCase() + if (isBedrockEdition() && BEDROCK_FCODES[lower]) return BEDROCK_FCODES[lower] + if (/[0-9a-f]/.test(lower)) return FCODES_TO_NAMES[lower] + if (BEDROCK_FCODES[lower]) return BEDROCK_FCODES[lower] + return FCODES_TO_NAMES[lower] + } + + function setEditionClass() { + document.body.classList.toggle('edition-bedrock', isBedrockEdition()) + document.body.classList.toggle('edition-java', !isBedrockEdition()) + } + function downloadTextFile(filename, text) { var element = document.createElement('a'); element.setAttribute('hidden', '') @@ -69,6 +180,7 @@ function App() { function storageSave() { localStorage.text = document.querySelector('#input').value + localStorage.edition = document.querySelector('#edition').value } function storageLoad() { @@ -76,26 +188,477 @@ function App() { localStorage.text = '' } document.querySelector('#input').value = localStorage.text + if (localStorage.edition) { + document.querySelector('#edition').value = localStorage.edition + } + setEditionClass() + } + + function rgbToHex(rgb) { + const r = (rgb >> 16) & 0xff + const g = (rgb >> 8) & 0xff + const b = rgb & 0xff + return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`.toUpperCase() + } + + function hexToRgb(hex) { + const clean = hex.replace('#', '') + if (!/^[0-9a-f]{6}$/i.test(clean)) return null + return parseInt(clean, 16) + } + + function lerpRgb(from, to, t) { + const fr = (from >> 16) & 0xff + const fg = (from >> 8) & 0xff + const fb = from & 0xff + const tr = (to >> 16) & 0xff + const tg = (to >> 8) & 0xff + const tb = to & 0xff + const r = Math.round(fr + (tr - fr) * t) + const g = Math.round(fg + (tg - fg) * t) + const b = Math.round(fb + (tb - fb) * t) + return (r << 16) | (g << 8) | b + } + + function gradientChars(text, colors) { + if (!text.length || !colors.length) return [] + if (colors.length === 1) { + return [...text].map(ch => ({ text: ch, color: colors[0] })) + } + + const chars = [...text] + const last = chars.length - 1 + return chars.map((ch, index) => { + const t = last === 0 ? 0 : index / last + const scaled = t * (colors.length - 1) + const segment = Math.min(Math.floor(scaled), colors.length - 2) + const localT = scaled - segment + return { + text: ch, + color: lerpRgb(colors[segment], colors[segment + 1], localT) + } + }) + } + + function buildTagGradient(text, colors) { + if (!text.length || colors.length < 2) return text + if (colors.length === 2) { + return `<${rgbToHex(colors[0])}>${text}<${rgbToHex(colors[1])}>` + } + const chars = [...text] + const segments = colors.length - 1 + const charsPerSegment = Math.ceil(chars.length / segments) + let result = '' + for (let s = 0; s < segments; s++) { + const chunk = chars.slice(s * charsPerSegment, (s + 1) * charsPerSegment).join('') + if (!chunk) continue + result += `<${rgbToHex(colors[s])}>${chunk}<${rgbToHex(colors[s + 1])}>` + } + return result + } + + function buildHexCodes(text, colors, formats) { + const chars = gradientChars(text, colors) + let result = '' + chars.forEach(({ text: ch, color }) => { + const hex = rgbToHex(color).slice(1) + let segment = `&#${hex}${ch}` + if (formats.bold) segment = `&#${hex}&l${ch}` + if (formats.italic) segment = `&#${hex}&o${ch}` + if (formats.underline) segment = `&#${hex}&n${ch}` + if (formats.strikethrough) segment = `&#${hex}&m${ch}` + result += segment + }) + return result + } + + function gradientToJson(text, colors, formats) { + const chars = gradientChars(text, colors) + const root = [] + chars.forEach(({ text: ch, color }) => { + const chunk = { text: ch, color: rgbToHex(color) } + if (formats.bold) chunk.bold = true + if (formats.italic) chunk.italic = true + if (formats.underline) chunk.underline = true + if (formats.strikethrough) chunk.strikethrough = true + root.push(chunk) + }) + return root + } + + function findTagEnd(input, start) { + let depth = 1 + for (let j = start + 1; j < input.length; j++) { + if (input[j] === '<') depth++ + if (input[j] === '>') { + depth-- + if (depth === 0) return j + } + } + return -1 + } + + function parseBracketLegacyCode(raw) { + const match = raw.trim().match(/^([&§])(.)$/i) + if (!match) return null + return match[2].toLowerCase() + } + + function parseBracketLegacyTag(raw) { + const code = parseBracketLegacyCode(raw) + if (code === null || !isLegacyColorCode(code)) return null + return code + } + + function parseBracketLegacyFormat(raw) { + const code = parseBracketLegacyCode(raw) + if (code === null || !isLegacyFormatCode(code)) return null + return FCODES_TO_NAMES[code] + } + + function legacyCodeToRgb(code) { + const name = legacyColorName(code) + if (name && NAMED_COLORS[name] !== undefined) return NAMED_COLORS[name] + return null + } + + function resolveTagColor(lower) { + const legacyCode = parseBracketLegacyTag(lower) + if (legacyCode !== null) return legacyCodeToRgb(legacyCode) + if (NAMED_COLORS[lower] !== undefined) return NAMED_COLORS[lower] + if (lower.startsWith('#')) { + const parsed = parseInt(lower.slice(1), 16) + return Number.isNaN(parsed) ? null : parsed & 0xffffff + } + if (/^[0-9a-f]{6,8}$/i.test(lower)) { + return parseInt(lower, 16) & 0xffffff + } + return null + } + + function decorFromState(state) { + const decor = {} + if (state.bold) decor.bold = true + if (state.italic) decor.italic = true + if (state.underline) decor.underline = true + if (state.strikethrough) decor.strikethrough = true + if (state.obfuscated) decor.obfuscated = true + return decor + } + + /** True for hex tags like <#RRGGBB> or legacy bracket tags like <&6>. */ + function isGradientEndpointTag(lower) { + if (lower.startsWith('#')) return true + if (/^[0-9a-f]{6,8}$/i.test(lower)) return true + if (parseBracketLegacyTag(lower) !== null) return true + return false + } + + /** Gradients apply between two endpoint tags on a single-line span. */ + function shouldApplyGradient(segment, fromIsEndpoint, toIsEndpoint) { + return ( + fromIsEndpoint && + toIsEndpoint && + segment.length > 0 && + segment.trim().length > 0 && + !segment.includes('\n') + ) + } + + /** Convert legacy & / § codes outside angle-bracket tags for mixed-format input. */ + function normalizeMixedInput(input) { + let result = '' + let i = 0 + while (i < input.length) { + if (input[i] === '<') { + const end = findTagEnd(input, i) + if (end !== -1) { + result += input.substring(i, end + 1) + i = end + 1 + continue + } + } + const prefix = input[i] + if ((prefix === '&' || prefix === '§') && i + 1 < input.length) { + const next = input[i + 1] + if (next === '#' && i + 7 < input.length) { + const hex = input.slice(i + 2, i + 8) + if (/^[0-9a-f]{6}$/i.test(hex)) { + result += `<#${hex.toUpperCase()}>` + i += 8 + continue + } + } + const code = next.toLowerCase() + if (code === 'r') { + result += '' + i += 2 + continue + } + if (FCODES_TO_NAMES[code]) { + result += `<${FCODES_TO_NAMES[code]}>` + i += 2 + continue + } + } + result += input[i] + i++ + } + return result + } + + function tagsToJson(input) { + const state = { + json: [], + current: null, + currentIsGradientEndpoint: false, + bold: false, + italic: false, + underline: false, + strikethrough: false, + obfuscated: false + } + + function pushChunk(text, color, decor) { + if (!text) return + const chunk = { text } + if (color !== null && color !== undefined) { + chunk.color = rgbToHex(color) + } + Object.assign(chunk, decor) + state.json.push(chunk) + } + + function pushGradient(text, fromRgb, toRgb, decor) { + gradientChars(text, [fromRgb, toRgb]).forEach(({ text: ch, color }) => { + pushChunk(ch, color, decor) + }) + } + + let textStart = 0 + let i = 0 + + while (i < input.length) { + if (input[i] !== '<') { + i++ + continue + } + + const end = findTagEnd(input, i) + if (end === -1) { + i++ + continue + } + + const raw = input.substring(i + 1, end).trim() + const lower = raw.toLowerCase() + + if (lower.startsWith('/')) { + const segment = input.substring(textStart, i) + const decor = decorFromState(state) + if (state.current !== null) pushChunk(segment, state.current, decor) + else pushChunk(segment, null, decor) + + switch (lower.slice(1).trim()) { + case 'bold': state.bold = false; break + case 'italic': state.italic = false; break + case 'underline': state.underline = false; break + case 'strikethrough': state.strikethrough = false; break + case 'obfuscated': state.obfuscated = false; break + } + + textStart = end + 1 + i = end + 1 + continue + } + + if (lower === 'r') { + const segment = input.substring(textStart, i) + const decor = decorFromState(state) + if (state.current !== null) pushChunk(segment, state.current, decor) + else pushChunk(segment, null, decor) + state.current = null + state.currentIsGradientEndpoint = false + state.bold = false + state.italic = false + state.underline = false + state.strikethrough = false + state.obfuscated = false + textStart = end + 1 + i = end + 1 + continue + } + + const colorVal = resolveTagColor(lower) + if (colorVal !== null) { + const segment = input.substring(textStart, i) + const decor = decorFromState(state) + const toIsEndpoint = isGradientEndpointTag(lower) + if (shouldApplyGradient(segment, state.currentIsGradientEndpoint, toIsEndpoint) && state.current !== null) { + pushGradient(segment, state.current, colorVal, decor) + } else if (segment) { + pushChunk(segment, state.current, decor) + } + state.current = colorVal + state.currentIsGradientEndpoint = toIsEndpoint + textStart = end + 1 + i = end + 1 + continue + } + + const bracketFormat = parseBracketLegacyFormat(lower) + if (bracketFormat) { + pushChunk(input.substring(textStart, i), state.current, decorFromState(state)) + switch (bracketFormat) { + case 'bold': state.bold = true; break + case 'italic': state.italic = true; break + case 'underline': state.underline = true; break + case 'strikethrough': state.strikethrough = true; break + case 'obfuscated': state.obfuscated = true; break + } + textStart = end + 1 + i = end + 1 + continue + } + + switch (lower) { + case 'bold': + pushChunk(input.substring(textStart, i), state.current, decorFromState(state)) + state.bold = true + textStart = end + 1 + i = end + 1 + continue + case 'italic': + pushChunk(input.substring(textStart, i), state.current, decorFromState(state)) + state.italic = true + textStart = end + 1 + i = end + 1 + continue + case 'underline': + pushChunk(input.substring(textStart, i), state.current, decorFromState(state)) + state.underline = true + textStart = end + 1 + i = end + 1 + continue + case 'strikethrough': + pushChunk(input.substring(textStart, i), state.current, decorFromState(state)) + state.strikethrough = true + textStart = end + 1 + i = end + 1 + continue + case 'obfuscated': + pushChunk(input.substring(textStart, i), state.current, decorFromState(state)) + state.obfuscated = true + textStart = end + 1 + i = end + 1 + continue + } + + i++ + } + + const tail = input.substring(textStart) + const decor = decorFromState(state) + if (state.current !== null) pushChunk(tail, state.current, decor) + else pushChunk(tail, null, decor) + + return state.json + } + + const TAG_PATTERN = /<(?:#?[0-9a-f]{6}|[&§][0-9a-fghijklmnopqrstuvw]|[a-z_]+)>/i + + function hasModernTags(text) { + return TAG_PATTERN.test(text) || /&#?[0-9a-f]{6}/i.test(text) + } + + function hexCodesToJson(text) { + const json = [] + let currentColor = null + let style = {} + let segment = '' + let i = 0 + + function flush() { + if (!segment) return + const chunk = { text: segment } + if (currentColor !== null) { + chunk.color = typeof currentColor === 'number' ? rgbToHex(currentColor) : currentColor + } + Object.assign(chunk, style) + json.push(chunk) + segment = '' + } + + while (i < text.length) { + const prefix = text[i] + if ((prefix === '&' || prefix === '§') && i + 1 < text.length) { + const next = text[i + 1] + if (next === '#' && i + 7 < text.length) { + const hex = text.slice(i + 2, i + 8) + if (/^[0-9a-f]{6}$/i.test(hex)) { + flush() + currentColor = parseInt(hex, 16) + i += 8 + continue + } + } + const code = next.toLowerCase() + if (/[0-9a-f]/.test(code) && FCODES_TO_NAMES[code]) { + flush() + currentColor = FCODES_TO_NAMES[code] + i += 2 + continue + } + if (/[klmnor]/.test(code) && FCODES_TO_NAMES[code]) { + flush() + const name = FCODES_TO_NAMES[code] + if (name === 'obfuscated' || name === 'bold' || name === 'strikethrough' || name === 'underline' || name === 'italic') { + style[name] = true + } + i += 2 + continue + } + if (code === 'r') { + flush() + currentColor = null + style = {} + i += 2 + continue + } + } + + segment += text[i] + i++ + } + + flush() + return json.filter(chunk => chunk.text.length > 0) } function fcodesToJson(text_fcodes) { + if (hasModernTags(text_fcodes)) { + if (TAG_PATTERN.test(text_fcodes)) { + return tagsToJson(normalizeMixedInput(text_fcodes)) + } + return hexCodesToJson(text_fcodes) + } + if (!/^[&§][0-9a-f]/gi.test(text_fcodes)) { text_fcodes = `&f${text_fcodes}` } text_fcodes = text_fcodes.replace(/[&§]r/gi, '&f') - let text_split = text_fcodes.match(/([&§][0-9a-fk-o](?:(?![&§][0-9a-fk-o])[\s\S])*)/gi) + let text_split = text_fcodes.match(legacySplitPattern()) let text_json = [] text_split.forEach(x => { let code = x.charAt(1) let text = x.slice(2) - if (/[0-9a-f]/i.test(code)) { + if (isLegacyColorCode(code)) { let json_chunk = {text: text} - json_chunk.color = FCODES_TO_NAMES[code] + json_chunk.color = legacyColorName(code) text_json.push(json_chunk) - } - if (/[k-o]/i.test(code)) { + } else if (isLegacyFormatCode(code)) { let json_chunk = {text: text} - json_chunk[FCODES_TO_NAMES[code]] = true + json_chunk[FCODES_TO_NAMES[code.toLowerCase()]] = true let nest = function(text_chunk) { if (text_chunk.extra) { nest(text_chunk.extra[text_chunk.extra.length - 1]) @@ -118,7 +681,11 @@ function App() { text = `&${NAMES_TO_FCODES[x]}${text}` } }) - text = `&${NAMES_TO_FCODES[json_chunk.color]}${text}` + if (json_chunk.color && json_chunk.color.startsWith('#')) { + text = `&#${json_chunk.color.slice(1)}${text}` + } else if (json_chunk.color) { + text = `&${NAMES_TO_FCODES[json_chunk.color]}${text}` + } text_fcodes = `${text_fcodes}${text}` if (json_chunk.extra) { json_chunk.extra.forEach(nest) @@ -138,11 +705,24 @@ function App() { return text } + function hexShadowColor(hex) { + const rgb = parseInt(hex.slice(1), 16) + const r = ((rgb >> 16) & 0xff) >> 2 + const g = ((rgb >> 8) & 0xff) >> 2 + const b = (rgb & 0xff) >> 2 + return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}` + } + function render(text_json) { let nest = function(json_chunk) { let rendered_chunk = document.createElement('span') rendered_chunk.innerHTML = escape(json_chunk.text) - rendered_chunk.classList.add(`formatting-${json_chunk.color}`) + if (json_chunk.color && json_chunk.color.startsWith('#')) { + rendered_chunk.style.color = json_chunk.color + rendered_chunk.style.textShadow = `1px 1px 0 ${hexShadowColor(json_chunk.color)}` + } else { + rendered_chunk.classList.add(`formatting-${json_chunk.color}`) + } ;['obfuscated', 'bold', 'strikethrough', 'underline', 'italic'].forEach(x => { if (json_chunk[x]) { rendered_chunk.classList.add(`formatting-${x}`) @@ -173,14 +753,156 @@ function App() { function update() { storageSave() + setEditionClass() let text_fcodes = document.querySelector('#input').value text_json_record = fcodesToJson(text_fcodes) render(text_json_record) } + function readGradientFormats() { + return { + bold: document.querySelector('#gradient-bold').checked, + italic: document.querySelector('#gradient-italic').checked, + underline: document.querySelector('#gradient-underline').checked, + strikethrough: document.querySelector('#gradient-strikethrough').checked + } + } + + function hslToRgb(h, s, l) { + const c = (1 - Math.abs(2 * l - 1)) * s + const x = c * (1 - Math.abs((h / 60) % 2 - 1)) + const m = l - c / 2 + let r = 0 + let g = 0 + let b = 0 + if (h < 60) { r = c; g = x } + else if (h < 120) { r = x; g = c } + else if (h < 180) { g = c; b = x } + else if (h < 240) { g = x; b = c } + else if (h < 300) { r = x; b = c } + else { r = c; b = x } + return ( + (Math.round((r + m) * 255) << 16) | + (Math.round((g + m) * 255) << 8) | + Math.round((b + m) * 255) + ) + } + + function randomGradientColor(hue) { + const h = hue ?? Math.random() * 360 + const s = 0.7 + Math.random() * 0.25 + const l = 0.38 + Math.random() * 0.22 + return rgbToHex(hslToRgb(h, s, l)) + } + + function randomizeGradientColors() { + const inputs = [...document.querySelectorAll('.gradient-color-input')] + const hueStart = Math.random() * 360 + const hueSpread = 80 + Math.random() * 220 + inputs.forEach((input, index) => { + const t = inputs.length === 1 ? 0 : index / (inputs.length - 1) + const hue = (hueStart + t * hueSpread) % 360 + input.value = randomGradientColor(hue) + }) + updateGradientPreview() + } + + function readGradientColors() { + return [...document.querySelectorAll('.gradient-color-input')].map(input => { + return hexToRgb(input.value) ?? 0xffffff + }) + } + + function updateGradientPreview() { + const text = document.querySelector('#gradient-text').value || 'Gradient' + const colors = readGradientColors() + const formats = readGradientFormats() + const preview = document.querySelector('#gradient-preview') + preview.innerHTML = '' + gradientChars(text, colors).forEach(({ text: ch, color }) => { + const span = document.createElement('span') + span.textContent = ch + span.style.color = rgbToHex(color) + if (formats.bold) span.classList.add('formatting-bold') + if (formats.italic) span.classList.add('formatting-italic') + if (formats.underline) span.classList.add('formatting-underline') + if (formats.strikethrough) span.classList.add('formatting-strikethrough') + preview.appendChild(span) + }) + + const format = document.querySelector('#gradient-format').value + const output = document.querySelector('#gradient-output') + if (format === 'tags') { + output.value = buildTagGradient(text, colors) + } else if (format === 'hex') { + output.value = buildHexCodes(text, colors, formats) + } else { + output.value = JSON.stringify(gradientToJson(text, colors, formats), null, 2) + } + } + + function addGradientColor() { + const list = document.querySelector('#gradient-colors') + const stops = list.querySelectorAll('.gradient-color-row') + if (stops.length >= 8) return + + const row = document.createElement('div') + row.className = 'gradient-color-row' + row.innerHTML = ` + + + ` + list.appendChild(row) + row.querySelector('.gradient-color-input').addEventListener('input', updateGradientPreview) + row.querySelector('.gradient-remove-color').addEventListener('click', () => { + if (list.querySelectorAll('.gradient-color-row').length <= 2) return + row.remove() + updateGradientPreview() + }) + updateGradientPreview() + } + + function applyGradient() { + const input = document.querySelector('#input') + const format = document.querySelector('#gradient-format').value + const text = document.querySelector('#gradient-text').value || 'Gradient' + const colors = readGradientColors() + const formats = readGradientFormats() + let insertion + if (format === 'hex') { + insertion = buildHexCodes(text, colors, formats) + } else { + insertion = buildTagGradient(text, colors) + } + input.value = input.value ? `${input.value}\n${insertion}` : insertion + update() + } + + function copyGradient() { + const output = document.querySelector('#gradient-output') + output.select() + document.execCommand('copy') + } + document.querySelector('#input').addEventListener('input', update) + document.querySelector('#edition').addEventListener('change', update) + document.querySelector('#gradient-text').addEventListener('input', updateGradientPreview) + document.querySelector('#gradient-format').addEventListener('change', updateGradientPreview) + document.querySelector('#gradient-bold').addEventListener('change', updateGradientPreview) + document.querySelector('#gradient-italic').addEventListener('change', updateGradientPreview) + document.querySelector('#gradient-underline').addEventListener('change', updateGradientPreview) + document.querySelector('#gradient-strikethrough').addEventListener('change', updateGradientPreview) + document.querySelectorAll('.gradient-color-input').forEach(input => { + input.addEventListener('input', updateGradientPreview) + }) + document.querySelector('#gradient-add-color').addEventListener('click', addGradientColor) + document.querySelector('#gradient-randomize').addEventListener('click', randomizeGradientColors) + document.querySelector('#gradient-apply').addEventListener('click', applyGradient) + document.querySelector('#gradient-copy').addEventListener('click', copyGradient) + storageLoad() update() + updateGradientPreview() document.querySelector('#import-json').addEventListener('change', importJson) document.querySelector('#export-json').addEventListener('click', exportJson) } diff --git a/formatting.css b/formatting.css index 849e34a..df8a191 100644 --- a/formatting.css +++ b/formatting.css @@ -77,3 +77,55 @@ .formatting-italic { font-style: italic; } +.formatting-minecoin_gold { + color: #DDD605; + text-shadow: 1px 1px 0 #373501; +} +.formatting-material_quartz { + color: #E3D4D1; + text-shadow: 1px 1px 0 #383534; +} +.formatting-material_iron { + color: #CECACA; + text-shadow: 1px 1px 0 #333232; +} +.formatting-material_netherite { + color: #443A3B; + text-shadow: 1px 1px 0 #110E0E; +} +.formatting-material_redstone { + color: #971607; + text-shadow: 1px 1px 0 #250501; +} +.formatting-material_copper { + color: #B4684D; + text-shadow: 1px 1px 0 #2D1A13; +} +.formatting-material_gold { + color: #DEB12D; + text-shadow: 1px 1px 0 #372C0B; +} +.formatting-material_emerald { + color: #119F36; + text-shadow: 1px 1px 0 #04280D; +} +.formatting-material_diamond { + color: #2CBAA8; + text-shadow: 1px 1px 0 #0B2E2A; +} +.formatting-material_lapis { + color: #21497B; + text-shadow: 1px 1px 0 #08121E; +} +.formatting-material_amethyst { + color: #9A5CC6; + text-shadow: 1px 1px 0 #261731; +} +.formatting-material_resin { + color: #EB7114; + text-shadow: 1px 1px 0 #3B1D05; +} +.formatting-party_blue { + color: #8CB3FF; + text-shadow: 1px 1px 0 #232D40; +} diff --git a/index.html b/index.html index 012e1ee..311957c 100644 --- a/index.html +++ b/index.html @@ -13,6 +13,7 @@
+
& @@ -36,10 +37,31 @@   k l - m - n + m + n o   + BE + g + h + i + j + m + n + p + q + s + t + u + v + w +   + + +  
@@ -56,6 +78,67 @@
+
+
+
+ Gradient Builder + Gradients: <#start>text<#end> or <&6>text<&5> legacy colors +
+
+
+ + +
+
+
+ +
+ Colors +
+
+
+ +
+
+ +
+
+
+ + +
+
+
+
+ Style +
+ + + + +
+
+
+
+ +
+
+
+ + + +
+ + +
+
+
+
+
diff --git a/style.css b/style.css index 9ef663f..213c390 100644 --- a/style.css +++ b/style.css @@ -23,14 +23,24 @@ a { flex: 1; } .main { + flex: 1; + display: flex; + flex-flow: column; + min-height: 0; + width: 100%; +} +.editor-row { flex: 1; display: flex; flex-flow: row; + min-height: 0; } .input-container, .output-container { flex: 1; display: flex; flex-flow: column; + min-height: 0; + min-width: 0; } .input-container { background: #fff; @@ -43,8 +53,293 @@ a { font-family: monospace; font-size: 1.2rem; } +#output, +#output span { + font-family: monospace; + font-size: 1.2rem; +} .input-main { border: none; overflow: auto; outline: none; + min-height: 0; +} +.input-nav, .output-nav { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.25rem; + padding: 0.5rem 0.75rem; + font-family: monospace; + font-size: 0.95rem; +} +.input-nav button, .output-nav button { + border: 1px solid #ccc; + background: #f7f7f7; + border-radius: 4px; + padding: 0.2rem 0.5rem; + cursor: pointer; +} +.input-nav-label, .edition-label { + font-family: sans-serif; + font-size: 0.75rem; + font-weight: 600; + color: #888; + text-transform: uppercase; + letter-spacing: 0.04em; +} +.edition-select { + font-family: sans-serif; + font-size: 0.82rem; + padding: 0.15rem 0.35rem; + border: 1px solid #ccc; + border-radius: 4px; + background: #fff; +} +body.edition-bedrock .java-mn-format { + display: none; +} +body.edition-java .bedrock-mn-color { + display: none; +} +.gradient-panel { + flex-shrink: 0; + width: 100%; + box-sizing: border-box; + border-top: 1px solid #ddd; + background: #f3f3f3; + padding: 0.85rem 1.25rem 1rem; + max-height: 42vh; + overflow-y: auto; +} +.gradient-panel-header { + display: flex; + flex-direction: column; + gap: 0.2rem; + margin-bottom: 0.85rem; +} +.gradient-panel-title { + font-weight: 600; + font-size: 0.95rem; + color: #222; +} +.gradient-panel-hint { + font-size: 0.8rem; + color: #666; +} +.gradient-panel-body { + display: flex; + flex-direction: column; + gap: 0.75rem; +} +.gradient-field { + display: grid; + grid-template-columns: 4.5rem minmax(0, 1fr); + gap: 0.75rem; + align-items: center; +} +.gradient-field-text { + background: #fff; + border: 1px solid #e0e0e0; + border-radius: 6px; + padding: 0.65rem 0.75rem; +} +.gradient-label { + font-size: 0.8rem; + font-weight: 600; + color: #555; + text-align: right; +} +.gradient-text-input { + width: 100%; + font-family: monospace; + font-size: 1rem; + padding: 0.5rem 0.65rem; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; +} +.gradient-main-grid { + display: grid; + grid-template-columns: minmax(13rem, 1fr) minmax(11rem, 1fr) minmax(15rem, 1.35fr); + gap: 0.75rem; + align-items: stretch; +} +.gradient-section { + background: #fff; + border: 1px solid #e0e0e0; + border-radius: 6px; + padding: 0.75rem; + display: flex; + flex-direction: column; + gap: 0.65rem; + min-width: 0; + min-height: 10.5rem; +} +.gradient-section-label { + margin: 0; + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.05em; + text-transform: uppercase; + color: #777; +} +.gradient-subfield { + display: grid; + grid-template-columns: 3.25rem minmax(0, 1fr); + gap: 0.5rem 0.65rem; + align-items: start; +} +.gradient-sublabel { + padding-top: 0.35rem; + font-size: 0.78rem; + font-weight: 500; + color: #666; + text-align: right; +} +.gradient-subfield-body { + display: flex; + flex-direction: column; + gap: 0.5rem; +} +.gradient-colors { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; + align-items: center; +} +.gradient-color-row { + display: flex; + align-items: center; + gap: 0.25rem; +} +.gradient-color-input { + width: 2.35rem; + height: 2.35rem; + padding: 0; + border: 1px solid #bbb; + border-radius: 4px; + cursor: pointer; + background: none; +} +.gradient-color-actions { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; +} +.gradient-remove-color { + width: 1.5rem; + height: 1.5rem; + border: 1px solid #ccc; + border-radius: 4px; + background: #fff; + color: #666; + cursor: pointer; + line-height: 1; + padding: 0; +} +.gradient-style-options { + display: flex; + flex-wrap: wrap; + gap: 0.55rem 0.85rem; + padding-top: 0.2rem; +} +.gradient-btn-secondary, .gradient-btn-primary { + border-radius: 4px; + padding: 0.35rem 0.7rem; + font-size: 0.82rem; + cursor: pointer; + border: 1px solid #ccc; + white-space: nowrap; +} +.gradient-btn-secondary { + background: #fafafa; + color: #333; +} +.gradient-btn-primary { + background: #333; + color: #fff; + border-color: #333; +} +.gradient-check { + font-size: 0.82rem; + color: #444; + display: inline-flex; + align-items: center; + gap: 0.3rem; + white-space: nowrap; +} +.gradient-section-preview, +.gradient-section-output { + min-height: 11.5rem; +} +.gradient-preview { + flex: 1; + width: 100%; + font-family: monospace; + font-size: 1.15rem; + line-height: 1.5; + padding: 0.75rem; + background: #fafafa; + border: 1px solid #ddd; + border-radius: 4px; + min-height: 5.5rem; + box-sizing: border-box; +} +.gradient-format-select { + width: 100%; + font-size: 0.82rem; + padding: 0.45rem 0.55rem; + border: 1px solid #ccc; + border-radius: 4px; + background: #fff; + box-sizing: border-box; +} +.gradient-output { + flex: 1; + width: 100%; + min-height: 4.5rem; + max-height: 7rem; + font-family: monospace; + font-size: 0.82rem; + padding: 0.55rem 0.65rem; + border: 1px solid #ccc; + border-radius: 4px; + resize: none; + box-sizing: border-box; + background: #fafafa; +} +.gradient-actions { + display: flex; + justify-content: flex-end; + gap: 0.45rem; + margin-top: auto; +} +@media (max-width: 960px) { + .gradient-main-grid { + grid-template-columns: 1fr 1fr; + } + .gradient-section-output { + grid-column: 1 / -1; + } + .gradient-section-preview { + order: -1; + } +} +@media (max-width: 620px) { + .gradient-field, + .gradient-subfield { + grid-template-columns: 1fr; + } + .gradient-label, + .gradient-sublabel { + text-align: left; + padding-top: 0; + } + .gradient-main-grid { + grid-template-columns: 1fr; + } + .gradient-section-preview { + order: -1; + } }