- commit
- b58d02501b9c7a845d1bfe50015b97ac81d739c9
- author
- cat <admin@dev.site>
- kind
- feat risk score 0.6 of 3, Jev's estimate
| A | README.md | 4 | + |
| A | index.html | 637 | ++++++++++++++++++++++++++++++ |
@@ -0,0 +1,637 @@
1+2+<html lang="en">3+<head>4+ <meta charset="utf-8">5+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">6+ <meta name="theme-color" content="#ece6da">7+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23eee8dc'/%3E%3Ccircle cx='0' cy='0' r='32' fill='%23c83b2e'/%3E%3C/svg%3E">8+ <meta name="description" content="A seeded Bauhaus pattern generator for reproducible tiled geometric compositions.">9+ <title>Bauhaus Pattern Generator</title>10+ <style>11+ :root {12+ color-scheme: light;13+ --button: rgba(245, 241, 232, 0.92);14+ --button-ink: #171717;15+ --focus: #fff;16+ }17+18+ * { box-sizing: border-box; }19+20+ html,21+ body {22+ width: 100%;23+ height: 100%;24+ margin: 0;25+ overflow: hidden;26+ background: #ece6da;27+ }28+29+ body {30+ overscroll-behavior: none;31+ touch-action: manipulation;32+ }33+34+ canvas {35+ display: block;36+ width: 100vw;37+ height: 100dvh;38+ }39+40+ .actions {41+ position: fixed;42+ z-index: 2;43+ right: max(14px, env(safe-area-inset-right));44+ bottom: max(14px, env(safe-area-inset-bottom));45+ display: flex;46+ gap: 8px;47+ }48+49+ button {50+ width: 44px;51+ height: 44px;52+ display: grid;53+ place-items: center;54+ padding: 0;55+ border: 1px solid rgba(23, 23, 23, 0.13);56+ border-radius: 50%;57+ color: var(--button-ink);58+ background: var(--button);59+ box-shadow: 0 2px 10px rgba(23, 23, 23, 0.12);60+ -webkit-backdrop-filter: blur(10px);61+ backdrop-filter: blur(10px);62+ cursor: pointer;63+ -webkit-tap-highlight-color: transparent;64+ }65+66+ button:hover { background: #fff; }67+ button:active { transform: translateY(1px); }68+ button:focus-visible {69+ outline: 3px solid var(--focus);70+ outline-offset: 2px;71+ }72+73+ button svg {74+ width: 18px;75+ height: 18px;76+ fill: none;77+ stroke: currentColor;78+ stroke-width: 1.8;79+ stroke-linecap: round;80+ stroke-linejoin: round;81+ }82+83+ .toast {84+ position: fixed;85+ z-index: 3;86+ right: max(14px, env(safe-area-inset-right));87+ bottom: calc(max(14px, env(safe-area-inset-bottom)) + 54px);88+ max-width: min(260px, calc(100vw - 28px));89+ padding: 8px 11px;90+ border-radius: 999px;91+ color: #171717;92+ background: rgba(245, 241, 232, 0.94);93+ box-shadow: 0 2px 10px rgba(23, 23, 23, 0.12);94+ font: 600 11px/1.2 ui-sans-serif, system-ui, sans-serif;95+ letter-spacing: 0.04em;96+ text-transform: uppercase;97+ opacity: 0;98+ transform: translateY(5px);99+ transition: opacity 160ms ease, transform 160ms ease;100+ pointer-events: none;101+ }102+103+ .toast.visible {104+ opacity: 1;105+ transform: translateY(0);106+ }107+108+ .sr-only {109+ position: absolute;110+ width: 1px;111+ height: 1px;112+ padding: 0;113+ margin: -1px;114+ overflow: hidden;115+ clip: rect(0, 0, 0, 0);116+ white-space: nowrap;117+ border: 0;118+ }119+120+ @media (prefers-reduced-transparency: reduce) {121+ button, .toast {122+ -webkit-backdrop-filter: none;123+ backdrop-filter: none;124+ background: #f5f1e8;125+ }126+ }127+ </style>128+</head>129+<body>130+ <canvas id="art" aria-label="A seeded tiled geometric composition"></canvas>131+132+ <div class="actions" aria-label="Artwork actions">133+ <button id="link" type="button" aria-label="Copy a permanent link to this generation" title="Copy link">134+ <svg viewBox="0 0 24 24" aria-hidden="true">135+ <path d="M10.2 13.8a4 4 0 0 0 5.7.1l2.8-2.8a4 4 0 0 0-5.6-5.6l-1.6 1.6"/>136+ <path d="M13.8 10.2a4 4 0 0 0-5.7-.1l-2.8 2.8a4 4 0 0 0 5.6 5.6l1.6-1.6"/>137+ </svg>138+ </button>139+ <button id="download" type="button" aria-label="Download this generation as a PNG" title="Download PNG">140+ <svg viewBox="0 0 24 24" aria-hidden="true">141+ <path d="M12 3v12"/>142+ <path d="m7.5 10.5 4.5 4.5 4.5-4.5"/>143+ <path d="M5 20h14"/>144+ </svg>145+ </button>146+ </div>147+148+ <div id="toast" class="toast" role="status" aria-live="polite"></div>149+ <p id="identity" class="sr-only"></p>150+151+ <script>152+ (() => {153+ 'use strict';154+155+ const canvas = document.querySelector('#art');156+ const ctx = canvas.getContext('2d', { alpha: false });157+ const linkButton = document.querySelector('#link');158+ const downloadButton = document.querySelector('#download');159+ const toast = document.querySelector('#toast');160+ const identity = document.querySelector('#identity');161+ const TAU = Math.PI * 2;162+163+ const paletteThemes = [164+ {165+ name: 'Bauhaus primary',166+ weight: 1.5,167+ variants: [168+ ['#eee8dc', '#171717', '#c83b2e', '#1e5484', '#dfb631'],169+ ['#e8e3d8', '#202020', '#b8342a', '#245b8c', '#e1ad25'],170+ ['#f1eadf', '#141414', '#cf432f', '#22518b', '#d9b13a'],171+ ['#e9e5dc', '#181818', '#c94332', '#315f8e', '#ddb934']172+ ]173+ },174+ {175+ name: 'Walnut and sage',176+ weight: 2,177+ lanes: [178+ { name: 'walnut', colors: ['#582f0e', '#7f4f24', '#936639', '#a68a64', '#b6ad90'] },179+ { name: 'sage', colors: ['#b6ad90', '#c2c5aa', '#a4ac86', '#656d4a', '#414833', '#333d29'] }180+ ]181+ },182+ {183+ name: 'Oxidized coast',184+ weight: 1.5,185+ lanes: [186+ { name: 'verdigris', colors: ['#001219', '#005f73', '#0a9396', '#94d2bd', '#e9d8a6'] },187+ { name: 'oxide', colors: ['#e9d8a6', '#ee9b00', '#ca6702', '#bb3e03', '#ae2012', '#9b2226'] }188+ ]189+ },190+ {191+ name: 'Harvest spectrum',192+ weight: 1,193+ lanes: [194+ { name: 'warm', colors: ['#f94144', '#f3722c', '#f8961e', '#f9844a', '#f9c74f'] },195+ { name: 'field', colors: ['#90be6d', '#43aa8b', '#4d908e', '#577590', '#277da1'] }196+ ]197+ },198+ {199+ name: 'Electric bloom',200+ weight: 1,201+ lanes: [202+ { name: 'bloom', colors: ['#f72585', '#b5179e', '#7209b7', '#560bad', '#480ca8'] },203+ { name: 'electric', colors: ['#3a0ca3', '#3f37c9', '#4361ee', '#4895ef', '#6daaf2'] }204+ ]205+ },206+ {207+ name: 'Desert grove',208+ weight: 2,209+ lanes: [210+ { name: 'grove', colors: ['#797d62', '#9b9b7a', '#baa587', '#d9ae94', '#f1dca7'] },211+ { name: 'clay', colors: ['#f1dca7', '#ffcb69', '#e8ac65', '#d08c60', '#b58463', '#997b66'] }212+ ]213+ },214+ {215+ name: 'Ember kiln',216+ weight: 1.5,217+ colors: ['#862906', '#a04005', '#b84f00', '#c46219', '#cb5925', '#dc5713', '#ed862b', '#875702', '#5a380b', '#411308']218+ },219+ {220+ name: 'Sunlit coast',221+ weight: 1.2,222+ lanes: [223+ { name: 'warm', colors: ['#cc4400', '#d66915', '#e08e29', '#f0c761', '#ffff99'] },224+ { name: 'cool', colors: ['#c2fcff', '#7cc6de', '#3890bc', '#1c489a', '#000077'] }225+ ]226+ }227+ ];228+229+ const motifNames = ['quarter', 'half', 'bar', 'diagonal', 'ring', 'inset', 'field', 'void'];230+ const grammars = ['weave', 'cadence', 'blocks'];231+ let seed = readSeedFromHash() || freshSeed();232+ let toastTimer = 0;233+ let resizeTimer = 0;234+ let renderInfo = null;235+236+ function normalizeSeed(value) {237+ const cleaned = String(value || '').trim().replace(/^#/, '');238+ return /^[a-zA-Z0-9_-]{1,64}$/.test(cleaned) ? cleaned : null;239+ }240+241+ function readSeedFromHash() {242+ return normalizeSeed(decodeURIComponent(location.hash.slice(1)));243+ }244+245+ function freshSeed() {246+ const words = new Uint32Array(2);247+ crypto.getRandomValues(words);248+ return Array.from(words, value => value.toString(16).padStart(8, '0')).join('');249+ }250+251+ function hash32(text) {252+ let hash = 2166136261;253+ for (let index = 0; index < text.length; index += 1) {254+ hash ^= text.charCodeAt(index);255+ hash = Math.imul(hash, 16777619);256+ }257+ hash += hash << 13;258+ hash ^= hash >>> 7;259+ hash += hash << 3;260+ hash ^= hash >>> 17;261+ hash += hash << 5;262+ return hash >>> 0;263+ }264+265+ function makeRng(path) {266+ let a = hash32(`${seed}|${path}|a`);267+ let b = hash32(`${seed}|${path}|b`);268+ let c = hash32(`${seed}|${path}|c`);269+ let d = hash32(`${seed}|${path}|d`);270+271+ const float = () => {272+ a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0;273+ const result = (a + b + d) >>> 0;274+ d = (d + 1) >>> 0;275+ a = b ^ (b >>> 9);276+ b = (c + (c << 3)) >>> 0;277+ c = ((c << 21) | (c >>> 11)) >>> 0;278+ c = (c + result) >>> 0;279+ return result / 4294967296;280+ };281+282+ return {283+ float,284+ int(min, max) { return Math.floor(float() * (max - min + 1)) + min; },285+ bool(chance = 0.5) { return float() < chance; },286+ pick(items) { return items[Math.floor(float() * items.length)]; },287+ weighted(entries) {288+ const total = entries.reduce((sum, entry) => sum + entry[1], 0);289+ let cursor = float() * total;290+ for (const [value, weight] of entries) {291+ cursor -= weight;292+ if (cursor <= 0) return value;293+ }294+ return entries[entries.length - 1][0];295+ }296+ };297+ }298+299+ function relativeLuminance(hex) {300+ const channels = [1, 3, 5].map(offset => parseInt(hex.slice(offset, offset + 2), 16) / 255);301+ const linear = channels.map(value => value <= 0.04045302+ ? value / 12.92303+ : ((value + 0.055) / 1.055) ** 2.4304+ );305+ return linear[0] * 0.2126 + linear[1] * 0.7152 + linear[2] * 0.0722;306+ }307+308+ function contrastRatio(first, second) {309+ const light = Math.max(relativeLuminance(first), relativeLuminance(second));310+ const dark = Math.min(relativeLuminance(first), relativeLuminance(second));311+ return (light + 0.05) / (dark + 0.05);312+ }313+314+ function buildPalette(rng) {315+ const theme = rng.weighted(paletteThemes.map(item => [item, item.weight]));316+ if (theme.variants) {317+ return { themeName: theme.name, palette: rng.pick(theme.variants) };318+ }319+320+ const lane = theme.lanes ? rng.pick(theme.lanes) : null;321+ const sourceColors = lane ? lane.colors : theme.colors;322+ const themeName = lane ? `${theme.name}, ${lane.name}` : theme.name;323+324+ const byLightness = sourceColors325+ .map(color => ({ color, lightness: relativeLuminance(color) }))326+ .sort((first, second) => first.lightness - second.lightness);327+ const ground = rng.pick(byLightness.slice(-3)).color;328+ let ink = rng.pick(byLightness.slice(0, 2)).color;329+330+ // Low-contrast families still need a structural dark to preserve the331+ // figure/ground tension that holds the composition together.332+ if (contrastRatio(ground, ink) < 3) {333+ const nativeDarkest = byLightness[0].color;334+ ink = contrastRatio(ground, nativeDarkest) >= 2.75 ? nativeDarkest : '#292823';335+ }336+337+ const pool = sourceColors.filter(color => color !== ground && color !== ink);338+ const accents = [];339+ while (accents.length < 3 && pool.length) {340+ accents.push(pool.splice(rng.int(0, pool.length - 1), 1)[0]);341+ }342+343+ return { themeName, palette: [ground, ink, ...accents] };344+ }345+346+ function buildSystem() {347+ const paletteRng = makeRng('palette');348+ const structureRng = makeRng('structure');349+ const rhythmRng = makeRng('rhythm');350+ const nestingRng = makeRng('nesting');351+ const { themeName, palette } = buildPalette(paletteRng);352+ const grammar = structureRng.pick(grammars);353+ const shortCount = structureRng.int(6, 11);354+ const period = structureRng.int(3, 6);355+ const blockSize = structureRng.pick([2, 2, 3]);356+ const mutationRate = structureRng.pick([0.035, 0.05, 0.07, 0.09]);357+ const motifSequence = Array.from({ length: period }, () => rhythmRng.pick(motifNames.slice(0, 7)));358+ const colorSequence = Array.from({ length: period + 1 }, () =>359+ rhythmRng.weighted([[0, 6], [1, 4], [2, 2], [3, 2], [4, 1.5]])360+ );361+ const direction = rhythmRng.pick([1, 1, -1]);362+ const nesting = {363+ enabled: nestingRng.bool(0.55),364+ maxDepth: nestingRng.weighted([[1, 3], [2, 1]]),365+ rootRate: nestingRng.pick([0.14, 0.18, 0.22, 0.26]),366+ branchRate: nestingRng.pick([0.12, 0.18, 0.24])367+ };368+369+ return {370+ palette, themeName, grammar, shortCount, period, blockSize, mutationRate,371+ motifSequence, colorSequence, direction, nesting372+ };373+ }374+375+ function setupCanvas() {376+ const cssWidth = Math.max(1, window.innerWidth);377+ const cssHeight = Math.max(1, window.innerHeight);378+ const dpr = Math.min(window.devicePixelRatio || 1, 4);379+ const pixelWidth = Math.max(1, Math.round(cssWidth * dpr));380+ const pixelHeight = Math.max(1, Math.round(cssHeight * dpr));381+382+ if (canvas.width !== pixelWidth || canvas.height !== pixelHeight) {383+ canvas.width = pixelWidth;384+ canvas.height = pixelHeight;385+ }386+ canvas.style.width = `${cssWidth}px`;387+ canvas.style.height = `${cssHeight}px`;388+ // Render directly in physical pixels. Keeping every shared edge on an389+ // integer backing-store coordinate prevents antialiased tile seams.390+ ctx.setTransform(1, 0, 0, 1, 0, 0);391+ ctx.imageSmoothingEnabled = true;392+ return { cssWidth, cssHeight, dpr, pixelWidth, pixelHeight };393+ }394+395+ function render() {396+ const viewport = setupCanvas();397+ const system = buildSystem();398+ const { pixelWidth: width, pixelHeight: height } = viewport;399+ const subdivisionUnit = 2 ** system.nesting.maxDepth;400+ const rawCell = Math.min(width, height) / system.shortCount;401+ const cell = Math.ceil(rawCell / subdivisionUnit) * subdivisionUnit;402+ const cols = Math.ceil(width / cell) + 2;403+ const rows = Math.ceil(height / cell) + 2;404+ const offsetX = Math.floor((width - cols * cell) / 2);405+ const offsetY = Math.floor((height - rows * cell) / 2);406+ system.minimumBranchSize = 42 * viewport.dpr;407+408+ ctx.fillStyle = system.palette[0];409+ ctx.fillRect(0, 0, width, height);410+411+ for (let row = 0; row < rows; row += 1) {412+ for (let col = 0; col < cols; col += 1) {413+ const x = offsetX + col * cell;414+ const y = offsetY + row * cell;415+ drawCell(ctx, system, col, row, x, y, cell);416+ }417+ }418+419+ renderInfo = { ...viewport, ...system, cols, rows, cell };420+ const topology = system.nesting.enabled421+ ? `nested square topology, depth ${system.nesting.maxDepth}`422+ : 'uniform square topology';423+ identity.textContent = `Seed ${seed}. ${system.themeName} colors. ${system.grammar} rhythm. ${topology}. ${system.shortCount} tiles on the short axis.`;424+ document.documentElement.style.setProperty('--focus', system.palette[4]);425+ document.querySelector('meta[name="theme-color"]').setAttribute('content', system.palette[0]);426+ }427+428+ function drawCell(context, system, col, row, x, y, size) {429+ const cadence = cadenceIndex(system, col, row);430+ drawSquareNode(context, system, col, row, x, y, size, `cell/${col}/${row}`, 0, cadence);431+ }432+433+ function drawSquareNode(context, system, col, row, x, y, size, path, depth, cadence) {434+ const nestingRng = makeRng(`nesting/${path}`);435+ const branchRate = depth === 0 ? system.nesting.rootRate : system.nesting.branchRate;436+ const canBranch = system.nesting.enabled437+ && depth < system.nesting.maxDepth438+ && size >= system.minimumBranchSize439+ && nestingRng.bool(branchRate);440+441+ if (canBranch) {442+ const childSize = size / 2;443+ for (let childRow = 0; childRow < 2; childRow += 1) {444+ for (let childCol = 0; childCol < 2; childCol += 1) {445+ const childIndex = childCol + childRow * 2;446+ const childCadence = (447+ cadence448+ + childCol449+ + childRow * system.direction450+ + depth451+ + 1452+ + system.period * 4453+ ) % system.period;454+ drawSquareNode(455+ context,456+ system,457+ col,458+ row,459+ x + childCol * childSize,460+ y + childRow * childSize,461+ childSize,462+ `${path}/branch-${depth}/${childIndex}`,463+ depth + 1,464+ childCadence465+ );466+ }467+ }468+ return;469+ }470+471+ drawSquareLeaf(context, system, col, row, x, y, size, path, depth, cadence);472+ }473+474+ function drawSquareLeaf(context, system, col, row, x, y, size, path, depth, cadence) {475+ const cellRng = makeRng(`${path}/leaf`);476+ let motif = system.motifSequence[cadence % system.motifSequence.length];477+ let rotation = ((cadence + (system.direction < 0 ? row : col) + depth) % 4 + 4) % 4;478+479+ if (cellRng.bool(system.mutationRate)) motif = cellRng.pick(motifNames);480+ if (cellRng.bool(system.mutationRate * 1.4)) rotation = cellRng.int(0, 3);481+482+ const backgroundIndex = system.colorSequence[cadence % system.colorSequence.length];483+ const foregroundIndex = chooseForeground(system, backgroundIndex, cadence, cellRng);484+ const background = system.palette[backgroundIndex];485+ const foreground = system.palette[foregroundIndex];486+487+ context.save();488+ context.beginPath();489+ context.rect(x, y, size, size);490+ context.clip();491+ context.fillStyle = background;492+ context.fillRect(x, y, size, size);493+ context.translate(x + size / 2, y + size / 2);494+ context.rotate(rotation * Math.PI / 2);495+ context.translate(-size / 2, -size / 2);496+ context.fillStyle = foreground;497+ drawMotif(context, motif, size, cellRng);498+ context.restore();499+ }500+501+ function cadenceIndex(system, col, row) {502+ if (system.grammar === 'weave') {503+ return (row + (col % 2) * 2) % system.period;504+ }505+ if (system.grammar === 'blocks') {506+ return (Math.floor(col / system.blockSize) + 2 * Math.floor(row / system.blockSize)) % system.period;507+ }508+ return (col + system.direction * row * 2 + system.period * 8) % system.period;509+ }510+511+ function chooseForeground(system, backgroundIndex, cadence, rng) {512+ const preferred = system.colorSequence[(cadence + 1) % system.colorSequence.length];513+ if (preferred !== backgroundIndex && rng.bool(0.72)) return preferred;514+ const candidates = [0, 1, 2, 3, 4].filter(index => index !== backgroundIndex);515+ if (backgroundIndex >= 2 && backgroundIndex <= 4) {516+ return rng.weighted([[0, 3], [1, 5]]);517+ }518+ return rng.pick(candidates);519+ }520+521+ function drawMotif(context, motif, size, rng) {522+ const half = size / 2;523+ switch (motif) {524+ case 'quarter':525+ context.beginPath();526+ context.arc(0, 0, size, 0, TAU);527+ context.fill();528+ break;529+ case 'half':530+ context.beginPath();531+ context.arc(half, size, half, Math.PI, TAU);532+ context.fill();533+ break;534+ case 'bar': {535+ const width = size * rng.pick([0.25, 0.333333, 0.5]);536+ context.fillRect((size - width) / 2, 0, width, size);537+ break;538+ }539+ case 'diagonal':540+ context.beginPath();541+ context.moveTo(0, 0);542+ context.lineTo(size, 0);543+ context.lineTo(size, size);544+ context.closePath();545+ context.fill();546+ break;547+ case 'ring': {548+ const ring = size * rng.pick([0.14, 0.18, 0.22]);549+ context.beginPath();550+ context.arc(half, half, size * 0.38, 0, TAU);551+ context.arc(half, half, Math.max(1, size * 0.38 - ring), 0, TAU, true);552+ context.fill('evenodd');553+ break;554+ }555+ case 'inset': {556+ const inset = size * rng.pick([0.18, 0.24, 0.3]);557+ context.fillRect(inset, inset, size - inset * 2, size - inset * 2);558+ break;559+ }560+ case 'field':561+ context.fillRect(0, 0, size, size);562+ break;563+ case 'void':564+ default:565+ break;566+ }567+ }568+569+ function showToast(message) {570+ window.clearTimeout(toastTimer);571+ toast.textContent = message;572+ toast.classList.add('visible');573+ toastTimer = window.setTimeout(() => toast.classList.remove('visible'), 1800);574+ }575+576+ async function copyPermanentLink() {577+ const url = new URL(location.href);578+ url.hash = encodeURIComponent(seed);579+ history.replaceState(null, '', url);580+581+ try {582+ await navigator.clipboard.writeText(url.href);583+ } catch (_) {584+ const input = document.createElement('textarea');585+ input.value = url.href;586+ input.setAttribute('readonly', '');587+ input.style.position = 'fixed';588+ input.style.opacity = '0';589+ document.body.appendChild(input);590+ input.select();591+ document.execCommand('copy');592+ input.remove();593+ }594+ showToast('Link copied · generation pinned');595+ }596+597+ function downloadPng() {598+ if (!renderInfo) return;599+ canvas.toBlob(blob => {600+ if (!blob) {601+ showToast('Download unavailable');602+ return;603+ }604+ const url = URL.createObjectURL(blob);605+ const anchor = document.createElement('a');606+ anchor.href = url;607+ anchor.download = `bauhaus-pattern-${seed}-${renderInfo.pixelWidth}x${renderInfo.pixelHeight}.png`;608+ document.body.appendChild(anchor);609+ anchor.click();610+ anchor.remove();611+ window.setTimeout(() => URL.revokeObjectURL(url), 1000);612+ showToast(`${renderInfo.pixelWidth} × ${renderInfo.pixelHeight} PNG`);613+ }, 'image/png');614+ }615+616+ function scheduleRender() {617+ window.clearTimeout(resizeTimer);618+ resizeTimer = window.setTimeout(render, 90);619+ }620+621+ linkButton.addEventListener('click', copyPermanentLink);622+ downloadButton.addEventListener('click', downloadPng);623+ window.addEventListener('resize', scheduleRender, { passive: true });624+ window.addEventListener('orientationchange', scheduleRender, { passive: true });625+ window.addEventListener('hashchange', () => {626+ const nextSeed = readSeedFromHash();627+ if (nextSeed && nextSeed !== seed) {628+ seed = nextSeed;629+ render();630+ }631+ });632+633+ render();634+ })();635+ </script>636+</body>637+</html>