@7ots/cli 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +17 -1
- package/cli/7ots.mjs +144 -36
- package/cli/lib/assistant.mjs +347 -0
- package/cli/lib/brain.mjs +10 -7
- package/cli/lib/config.mjs +8 -1
- package/cli/lib/i18n.mjs +84 -11
- package/cli/lib/install.mjs +29 -1
- package/cli/lib/pet-core.mjs +3 -2
- package/cli/lib/pet-server.mjs +133 -13
- package/cli/lib/prompts.mjs +199 -7
- package/cli/lib/terminal.mjs +27 -6
- package/cli/pet/electron/main.cjs +96 -32
- package/cli/pet/pet.html +59 -6
- package/cli/pet/settings.html +430 -0
- package/dist/7ots.esm.js +370 -142
- package/dist/7ots.esm.js.map +4 -4
- package/dist/7ots.iife.js +370 -142
- package/dist/7ots.iife.js.map +4 -4
- package/llms.txt +198 -6
- package/package.json +1 -1
- package/server/channels/apuchat.mjs +8 -5
- package/server/channels/apumail.mjs +13 -3
- package/server/channels/index.mjs +5 -3
- package/server/platform/apuchat.mjs +266 -0
- package/server/platform/auth.mjs +39 -10
- package/server/platform/connect.mjs +347 -0
- package/server/platform/db.mjs +55 -0
- package/server/platform/look.mjs +20 -0
- package/server/platform/orquesta.mjs +499 -0
- package/server/platform/routes.mjs +57 -8
- package/server/platform/runtime.mjs +75 -15
- package/server/platform/store.mjs +130 -9
- package/server/server.mjs +14 -3
- package/src/avatar/AvatarStage.js +54 -9
- package/src/avatar/three.js +62 -0
- package/src/character/AvatarEditor.js +174 -50
- package/src/character/Character.js +164 -26
- package/src/character/motion.js +95 -34
- package/src/character/parts.js +43 -12
- package/src/character/skin.js +412 -0
- package/src/character/styles.js +1429 -0
- package/src/character3d/Character3D.js +1693 -0
- package/src/character3d/body.js +154 -0
- package/src/character3d/fx.js +323 -0
- package/src/character3d/index.js +281 -0
- package/src/character3d/materials.js +429 -0
- package/src/character3d/stage.js +187 -0
- package/src/i18n/messages/character.js +18 -42
- package/src/i18n/messages/platform.js +135 -0
- package/src/identity/schema.js +16 -8
- package/src/index.js +1 -0
|
@@ -0,0 +1,1429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Estilos artísticos («mundos») del personaje 2D: el mismo ot redibujado como grabado ukiyo-e,
|
|
3
|
+
* plastilina, vitral, Bauhaus, manuscrito iluminado, glitch, boceto a tinta, pop art, acuarela,
|
|
4
|
+
* píxel 8 bits, synthwave, art nouveau, cubismo, crayones, plano técnico, puntillismo o xilografía,
|
|
5
|
+
* con un fondo a juego.
|
|
6
|
+
*
|
|
7
|
+
* const spec = normalizeCharacter({ ...ot, style: 'ukiyoe' }); // se guarda en spec.style
|
|
8
|
+
* el.innerHTML = renderCharacter(spec); // estilo + fondo
|
|
9
|
+
* createCharacter(el, spec); // vivo: respira y parpadea igual
|
|
10
|
+
*
|
|
11
|
+
* Cada estilo aporta, todo procedural (SVG/CSS, sin dependencias):
|
|
12
|
+
* · paleta: remapea los colores de la spec (stylizeSpec) y fija acabado, textura y contorno;
|
|
13
|
+
* · capa interior: algo dibujado dentro del cuerpo (vetas, plomos del vitral, mitades Bauhaus…);
|
|
14
|
+
* · filtro: <filter> sobre todo el personaje (grano, posterizado, luz de plastilina, píxeles…);
|
|
15
|
+
* · fondo: escena en un marco fijo de 300×300 (el suelo empieza en y≈240), determinista con la
|
|
16
|
+
* semilla del ot (styleSeed): el mismo ot siempre tiene el mismo paisaje.
|
|
17
|
+
*
|
|
18
|
+
* 'none' (o ausente, specs antiguas) no cambia nada. Los filtros tienen versión 'lite' (sin ruido
|
|
19
|
+
* ni luces) para cuando el contorno se anima, en equipos modestos o con movimiento reducido.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { luminance, hexToRgb } from './parts.js';
|
|
23
|
+
import { skinHash } from './skin.js';
|
|
24
|
+
import { seeded } from './motion.js';
|
|
25
|
+
import { addCatalog } from '../i18n/index.js';
|
|
26
|
+
|
|
27
|
+
const n = (v) => Math.round(v * 100) / 100;
|
|
28
|
+
|
|
29
|
+
export const STYLES = [
|
|
30
|
+
{ id: 'none', label: { es: 'Ninguno', en: 'None', pt: 'Nenhum' } },
|
|
31
|
+
{ id: 'ukiyoe', label: { es: 'Ukiyo-e', en: 'Ukiyo-e', pt: 'Ukiyo-e' } },
|
|
32
|
+
{ id: 'clay', label: { es: 'Plastilina', en: 'Claymation', pt: 'Massinha' } },
|
|
33
|
+
{ id: 'glass', label: { es: 'Vitral', en: 'Stained glass', pt: 'Vitral' } },
|
|
34
|
+
{ id: 'bauhaus', label: { es: 'Bauhaus', en: 'Bauhaus', pt: 'Bauhaus' } },
|
|
35
|
+
{ id: 'manuscript', label: { es: 'Manuscrito', en: 'Manuscript', pt: 'Iluminura' } },
|
|
36
|
+
{ id: 'glitch', label: { es: 'Glitch', en: 'Glitch', pt: 'Glitch' } },
|
|
37
|
+
{ id: 'ink', label: { es: 'Tinta', en: 'Ink sketch', pt: 'Nanquim' } },
|
|
38
|
+
{ id: 'pop', label: { es: 'Pop art', en: 'Pop art', pt: 'Pop art' } },
|
|
39
|
+
{ id: 'watercolor', label: { es: 'Acuarela', en: 'Watercolor', pt: 'Aquarela' } },
|
|
40
|
+
{ id: 'pixel', label: { es: 'Píxel 8 bits', en: '8-bit pixel', pt: 'Pixel 8 bits' } },
|
|
41
|
+
{ id: 'synthwave', label: { es: 'Synthwave', en: 'Synthwave', pt: 'Synthwave' } },
|
|
42
|
+
{ id: 'nouveau', label: { es: 'Art nouveau', en: 'Art nouveau', pt: 'Art nouveau' } },
|
|
43
|
+
{ id: 'cubism', label: { es: 'Cubismo', en: 'Cubism', pt: 'Cubismo' } },
|
|
44
|
+
{ id: 'crayon', label: { es: 'Crayones', en: 'Crayon', pt: 'Giz de cera' } },
|
|
45
|
+
{ id: 'blueprint', label: { es: 'Plano técnico', en: 'Blueprint', pt: 'Planta técnica' } },
|
|
46
|
+
{ id: 'pointillism', label: { es: 'Puntillismo', en: 'Pointillism', pt: 'Pontilhismo' } },
|
|
47
|
+
{ id: 'woodcut', label: { es: 'Xilografía', en: 'Woodcut', pt: 'Xilogravura' } },
|
|
48
|
+
];
|
|
49
|
+
export const STYLE_IDS = STYLES.map((s) => s.id);
|
|
50
|
+
|
|
51
|
+
const lang = (l) => ({
|
|
52
|
+
character: { styles: Object.fromEntries(STYLES.map((s) => [s.id, s.label[l]])) },
|
|
53
|
+
editor: {
|
|
54
|
+
artStyle: {
|
|
55
|
+
es: { title: 'Estilo artístico', hint: 'Redibuja el mismo personaje en otro estilo, con un fondo a juego.' },
|
|
56
|
+
en: { title: 'Art style', hint: 'Redraws the same character in another art style, with a matching backdrop.' },
|
|
57
|
+
pt: { title: 'Estilo artístico', hint: 'Redesenha o mesmo personagem em outro estilo, com um fundo combinando.' },
|
|
58
|
+
}[l],
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
addCatalog({ es: lang('es'), en: lang('en'), pt: lang('pt') });
|
|
62
|
+
|
|
63
|
+
// ───────────────────────────── color ─────────────────────────────
|
|
64
|
+
|
|
65
|
+
const toHex = (rgb) => `#${rgb.map((v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, '0')).join('')}`;
|
|
66
|
+
|
|
67
|
+
function toHsl(hex) {
|
|
68
|
+
const [r, g, b] = hexToRgb(hex).map((v) => v / 255);
|
|
69
|
+
const mx = Math.max(r, g, b);
|
|
70
|
+
const mn = Math.min(r, g, b);
|
|
71
|
+
const l = (mx + mn) / 2;
|
|
72
|
+
if (mx === mn) return [0, 0, l];
|
|
73
|
+
const d = mx - mn;
|
|
74
|
+
const s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn);
|
|
75
|
+
const h = mx === r ? (g - b) / d + (g < b ? 6 : 0) : mx === g ? (b - r) / d + 2 : (r - g) / d + 4;
|
|
76
|
+
return [h * 60, s, l];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function fromHsl(h, s, l) {
|
|
80
|
+
const a = s * Math.min(l, 1 - l);
|
|
81
|
+
const f = (k) => {
|
|
82
|
+
const t = (k + h / 30) % 12;
|
|
83
|
+
return 255 * (l - a * Math.max(-1, Math.min(t - 3, 9 - t, 1)));
|
|
84
|
+
};
|
|
85
|
+
return toHex([f(0), f(8), f(4)]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Distancia «redmean» entre dos colores (barata y bastante perceptual). */
|
|
89
|
+
function dist(a, b) {
|
|
90
|
+
const [r1, g1, b1] = hexToRgb(a);
|
|
91
|
+
const [r2, g2, b2] = hexToRgb(b);
|
|
92
|
+
const rm = (r1 + r2) / 2;
|
|
93
|
+
return (2 + rm / 256) * (r1 - r2) ** 2 + 4 * (g1 - g2) ** 2 + (2 + (255 - rm) / 256) * (b1 - b2) ** 2;
|
|
94
|
+
}
|
|
95
|
+
const nearest = (hex, list, avoid = []) => {
|
|
96
|
+
const pool = list.filter((c) => !avoid.includes(c));
|
|
97
|
+
return pool.reduce((best, c) => (dist(hex, c) < dist(hex, best) ? c : best), pool[0]);
|
|
98
|
+
};
|
|
99
|
+
const grey = (hex, lo, hi) => {
|
|
100
|
+
const v = Math.round(255 * (lo + luminance(hex) * (hi - lo)));
|
|
101
|
+
return toHex([v, v, v]);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const PAL = {
|
|
105
|
+
ukiyoe: ['#1f3a5f', '#3f6e8c', '#c0392b', '#d98e4a', '#e6c27a', '#7a9a6a', '#e9dcc0', '#d9a0a0', '#2b2b2b'],
|
|
106
|
+
glass: ['#1e5aa8', '#b8222a', '#e2a52a', '#2f8a4a', '#6a2c8c', '#2aa0b8', '#d65a1f'],
|
|
107
|
+
bauhaus: ['#d7262b', '#f2b705', '#1f4e9c'],
|
|
108
|
+
manuscript: ['#1d3f8f', '#b3261e', '#2e6b3a', '#c99a2e', '#6b2c5a', '#e9d9b4'],
|
|
109
|
+
pop: ['#ffd400', '#e4002b', '#0072ce', '#ff8ab4', '#00a651', '#ffffff', '#111111'],
|
|
110
|
+
// subconjunto de la paleta de la NES
|
|
111
|
+
pixel: ['#000000', '#fcfcfc', '#bcbcbc', '#7c7c7c', '#a81000', '#f83800', '#e45c10', '#fca044', '#f8b800', '#ac7c00', '#00a800', '#58d854', '#005800', '#0058f8', '#3cbcfc', '#6844fc', '#d800cc', '#f878f8', '#fcd8a8'],
|
|
112
|
+
synthwave: ['#ff2bd6', '#2bf3ff', '#8a2bff', '#ff8a2b', '#ffe12b', '#ff2b6e', '#4b3bff'],
|
|
113
|
+
nouveau: ['#8a9a6b', '#c98a7a', '#c9a24a', '#4f7a74', '#efe2c4', '#b5654a', '#6e4a5e', '#a8b48c', '#d9b98a'],
|
|
114
|
+
cubism: ['#c8a046', '#7a5232', '#7c7a46', '#4e6a86', '#b5583a', '#e6d6b0', '#2e2a26', '#8aa0a8'],
|
|
115
|
+
crayon: ['#e8352b', '#f58a1f', '#f6d32b', '#4caf3c', '#2f6fd6', '#8a4fc4', '#f27bb5', '#8b5a2b', '#5fc8e8', '#222222'],
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** Mezcla lineal de dos colores (t=0 → a). */
|
|
119
|
+
const mix = (a, b, t) => {
|
|
120
|
+
const x = hexToRgb(a);
|
|
121
|
+
const y = hexToRgb(b);
|
|
122
|
+
return toHex(x.map((v, i) => v + (y[i] - v) * t));
|
|
123
|
+
};
|
|
124
|
+
/** Mismo tono con saturación y luz acotadas. */
|
|
125
|
+
const clampHsl = (c, s0, s1, l0, l1) => {
|
|
126
|
+
const [h, s, l] = toHsl(c);
|
|
127
|
+
return fromHsl(h, Math.min(s1, Math.max(s0, s)), Math.min(l1, Math.max(l0, l)));
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/** Primario Bauhaus de un color: por tono (los grises van a negro o crema). */
|
|
131
|
+
function bauhausOf(hex) {
|
|
132
|
+
const [h, s, l] = toHsl(hex);
|
|
133
|
+
if (s < 0.18) return l > 0.62 ? '#f1ead8' : '#151515';
|
|
134
|
+
if (h < 22 || h >= 285) return '#d7262b';
|
|
135
|
+
if (h < 150) return '#f2b705';
|
|
136
|
+
return '#1f4e9c';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Remapeo de paleta y ajustes de cada estilo. `ink`/`lw`: contorno propio del estilo (lw 0 = sin contorno).
|
|
141
|
+
* `map(color, rol)` recibe el rol: 'body' | 'pattern' | 'iris' | 'cheek' | 'lip' | 'brow' | 'acc'.
|
|
142
|
+
*/
|
|
143
|
+
const LOOKS = {
|
|
144
|
+
ukiyoe: {
|
|
145
|
+
finish: 'flat', texture: 'none', ink: '#1b2233', lw: 3.4,
|
|
146
|
+
eyes: { white: '#f3ead6', ink: '#1b1b1b', iris: '#1f3a5f' },
|
|
147
|
+
map: (c, role) => (role === 'cheek' ? '#d9776a' : nearest(c, PAL.ukiyoe, role === 'pattern' ? [] : role === 'body' ? ['#2b2b2b', '#e9dcc0'] : ['#2b2b2b'])),
|
|
148
|
+
},
|
|
149
|
+
clay: {
|
|
150
|
+
finish: 'clay', texture: 'none', ink: '', lw: 0,
|
|
151
|
+
eyes: {},
|
|
152
|
+
map: (c, role) => {
|
|
153
|
+
if (role === 'iris') return c;
|
|
154
|
+
const [h, s, l] = toHsl(c);
|
|
155
|
+
return fromHsl(h, Math.min(0.72, Math.max(0.38, s)), Math.min(0.74, Math.max(0.46, l)));
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
glass: {
|
|
159
|
+
finish: 'flat', texture: 'none', ink: '#141014', lw: 5,
|
|
160
|
+
eyes: { white: '#f3ecd0', ink: '#141014' },
|
|
161
|
+
map: (c, role, spec) => (role === 'pattern' ? nearest(c, PAL.glass, [nearest(spec.body.color, PAL.glass)]) : nearest(c, PAL.glass)),
|
|
162
|
+
},
|
|
163
|
+
bauhaus: {
|
|
164
|
+
finish: 'flat', texture: 'none', ink: '#151515', lw: 4.5,
|
|
165
|
+
eyes: { white: '#ffffff', ink: '#151515', iris: '#151515' },
|
|
166
|
+
map: (c, role, spec) => {
|
|
167
|
+
if (role === 'cheek' || role === 'lip') return '#d7262b';
|
|
168
|
+
const p = bauhausOf(c);
|
|
169
|
+
if (role === 'pattern') {
|
|
170
|
+
const b = bauhausOf(spec.body.color);
|
|
171
|
+
return p === b ? PAL.bauhaus[(PAL.bauhaus.indexOf(b) + 1) % 3] || '#f2b705' : p;
|
|
172
|
+
}
|
|
173
|
+
return p;
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
manuscript: {
|
|
177
|
+
finish: 'flat', texture: 'none', ink: '#3b2412', lw: 3,
|
|
178
|
+
eyes: { white: '#f5ead0', ink: '#2a1a0e' },
|
|
179
|
+
map: (c, role) => (role === 'cheek' ? '#c8584a' : nearest(c, PAL.manuscript, role === 'body' ? ['#e9d9b4'] : [])),
|
|
180
|
+
},
|
|
181
|
+
glitch: {
|
|
182
|
+
finish: 'flat', texture: 'none', ink: '#0b0b14', lw: 2.4,
|
|
183
|
+
eyes: {},
|
|
184
|
+
map: (c, role) => {
|
|
185
|
+
if (role === 'iris') return c;
|
|
186
|
+
const [h, s, l] = toHsl(c);
|
|
187
|
+
return fromHsl(h, Math.max(0.78, s), Math.min(0.66, Math.max(0.46, l)));
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
ink: {
|
|
191
|
+
finish: 'sketch', texture: 'none', ink: '#222222', lw: 2.6,
|
|
192
|
+
eyes: { white: '#ffffff', ink: '#1e1e1e', iris: '#3a3a3a' },
|
|
193
|
+
map: (c, role) => {
|
|
194
|
+
if (role === 'body') return grey(c, 0.88, 0.99);
|
|
195
|
+
if (role === 'pattern') return '#ddd8cc';
|
|
196
|
+
if (role === 'cheek' || role === 'lip') return '#b9b2a6';
|
|
197
|
+
return grey(c, 0.3, 0.92);
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
pop: {
|
|
201
|
+
finish: 'flat', texture: 'none', ink: '#111111', lw: 5,
|
|
202
|
+
eyes: { white: '#ffffff', ink: '#111111' },
|
|
203
|
+
map: (c, role) => (role === 'cheek' || role === 'lip' ? '#e4002b' : nearest(c, PAL.pop, role === 'body' ? ['#ffffff', '#111111'] : role === 'brow' ? [] : ['#ffffff'])),
|
|
204
|
+
},
|
|
205
|
+
watercolor: {
|
|
206
|
+
finish: 'flat', texture: 'none', ink: '#5b4a3c', lw: 1.3,
|
|
207
|
+
eyes: { white: '#fffdf6', ink: '#3b3029' },
|
|
208
|
+
map: (c, role) => (role === 'iris' || role === 'brow' ? clampHsl(c, 0.3, 0.6, 0.25, 0.5) : clampHsl(c, 0.42, 0.72, 0.5, 0.72)),
|
|
209
|
+
},
|
|
210
|
+
pixel: {
|
|
211
|
+
finish: 'flat', texture: 'none', ink: '#000000', lw: 4.5,
|
|
212
|
+
eyes: { white: '#fcfcfc', ink: '#000000' },
|
|
213
|
+
map: (c, role) => nearest(c, PAL.pixel, role === 'body' ? ['#000000', '#7c7c7c'] : []),
|
|
214
|
+
},
|
|
215
|
+
synthwave: {
|
|
216
|
+
finish: 'flat', texture: 'none', ink: '#fdf6ff', lw: 3.2,
|
|
217
|
+
eyes: { white: '#fdf6ff', ink: '#1a0b33' },
|
|
218
|
+
map: (c, role, spec) => {
|
|
219
|
+
if (role === 'pattern') return nearest(c, PAL.synthwave, [nearest(spec.body.color, PAL.synthwave)]);
|
|
220
|
+
if (role === 'brow') return '#1a0b33';
|
|
221
|
+
return nearest(c, PAL.synthwave);
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
nouveau: {
|
|
225
|
+
finish: 'flat', texture: 'none', ink: '#4a3324', lw: 3,
|
|
226
|
+
eyes: { white: '#f6ecd6', ink: '#3a281c' },
|
|
227
|
+
map: (c, role) => (role === 'cheek' ? '#d08e7e' : nearest(c, PAL.nouveau, role === 'body' ? ['#efe2c4'] : [])),
|
|
228
|
+
},
|
|
229
|
+
cubism: {
|
|
230
|
+
finish: 'flat', texture: 'none', ink: '#2e2a26', lw: 3.4,
|
|
231
|
+
eyes: { white: '#efe4c8', ink: '#2e2a26' },
|
|
232
|
+
map: (c, role) => nearest(c, PAL.cubism, role === 'body' ? ['#e6d6b0', '#2e2a26'] : []),
|
|
233
|
+
},
|
|
234
|
+
crayon: {
|
|
235
|
+
finish: 'flat', texture: 'none', ink: '#2a2a2a', lw: 3,
|
|
236
|
+
eyes: { white: '#ffffff', ink: '#222222' },
|
|
237
|
+
map: (c, role) => (role === 'cheek' ? '#f27bb5' : nearest(c, PAL.crayon, role === 'body' ? ['#222222'] : [])),
|
|
238
|
+
},
|
|
239
|
+
blueprint: {
|
|
240
|
+
finish: 'flat', texture: 'none', ink: '#eaf2ff', lw: 2.2,
|
|
241
|
+
eyes: { white: '#3a70b8', ink: '#16386a', iris: '#eaf2ff' },
|
|
242
|
+
map: (c, role) => {
|
|
243
|
+
if (role === 'body') return '#2e64ad';
|
|
244
|
+
if (role === 'pattern') return '#4a80c6';
|
|
245
|
+
if (role === 'cheek') return '#5a8fd0';
|
|
246
|
+
if (role === 'lip' || role === 'brow') return '#eaf2ff';
|
|
247
|
+
return luminance(c) > 0.55 ? '#bcd3f0' : '#4a80c6';
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
pointillism: {
|
|
251
|
+
finish: 'flat', texture: 'none', ink: '', lw: 0,
|
|
252
|
+
eyes: {},
|
|
253
|
+
map: (c, role) => (role === 'iris' || role === 'brow' ? c : clampHsl(c, 0.45, 0.75, 0.5, 0.7)),
|
|
254
|
+
},
|
|
255
|
+
woodcut: {
|
|
256
|
+
finish: 'flat', texture: 'none', ink: '#15120f', lw: 4.5,
|
|
257
|
+
eyes: { white: '#f1e6cf', ink: '#15120f', iris: '#15120f' },
|
|
258
|
+
map: (c, role) => {
|
|
259
|
+
if (role === 'body') return '#ecdfc4';
|
|
260
|
+
if (role === 'cheek' || role === 'lip') return '#b8352a';
|
|
261
|
+
if (role === 'pattern' || role === 'brow') return '#15120f';
|
|
262
|
+
return luminance(c) > 0.5 ? '#ecdfc4' : '#15120f';
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/** Semilla del estilo para un ot: la misma que su piel (no cambia al arrastrar puntos). */
|
|
268
|
+
export function styleSeed(spec) {
|
|
269
|
+
return skinHash(spec);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Spec lista para pintar con su estilo: colores remapeados, acabado/textura del estilo y
|
|
274
|
+
* `_style` ({ id, seed, ink, lw }) para el contorno. Sin estilo devuelve la misma spec.
|
|
275
|
+
* Recibe una spec normalizada; el resultado no se guarda (normalizeCharacter descarta `_style`).
|
|
276
|
+
*/
|
|
277
|
+
export function stylizeSpec(spec) {
|
|
278
|
+
const id = spec?.style;
|
|
279
|
+
const look = LOOKS[id];
|
|
280
|
+
if (!look) return spec;
|
|
281
|
+
const s = structuredClone(spec);
|
|
282
|
+
const m = (c, role) => (c ? look.map(c, role, spec) : c);
|
|
283
|
+
s.body.color = m(spec.body.color, 'body');
|
|
284
|
+
s.body.patternColor = m(spec.body.patternColor, 'pattern');
|
|
285
|
+
s.eyes.iris = m(spec.eyes.iris, 'iris');
|
|
286
|
+
Object.assign(s.eyes, look.eyes);
|
|
287
|
+
s.cheeks.color = m(spec.cheeks.color, 'cheek');
|
|
288
|
+
s.mouth.lipColor = m(spec.mouth.lipColor, 'lip');
|
|
289
|
+
if (spec.mouth.color) s.mouth.color = m(spec.mouth.color, 'brow');
|
|
290
|
+
if (spec.brows.color) s.brows.color = m(spec.brows.color, 'brow');
|
|
291
|
+
for (const a of s.accessories) {
|
|
292
|
+
a.color = m(a.color, 'acc');
|
|
293
|
+
a.color2 = m(a.color2, 'acc');
|
|
294
|
+
}
|
|
295
|
+
s.finish = look.finish;
|
|
296
|
+
s.texture = look.texture;
|
|
297
|
+
s._style = { id, seed: styleSeed(spec), ink: look.ink, lw: look.lw };
|
|
298
|
+
return s;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ───────────────────────────── geometría ─────────────────────────────
|
|
302
|
+
|
|
303
|
+
/** Recorta un polígono convexo al semiplano de los puntos más cerca de p que de q. */
|
|
304
|
+
function clipHalf(poly, p, q) {
|
|
305
|
+
const nx = q[0] - p[0];
|
|
306
|
+
const ny = q[1] - p[1];
|
|
307
|
+
const c = (nx * (p[0] + q[0])) / 2 + (ny * (p[1] + q[1])) / 2;
|
|
308
|
+
const f = (v) => nx * v[0] + ny * v[1] - c;
|
|
309
|
+
const out = [];
|
|
310
|
+
for (let i = 0; i < poly.length; i++) {
|
|
311
|
+
const a = poly[i];
|
|
312
|
+
const b = poly[(i + 1) % poly.length];
|
|
313
|
+
const fa = f(a);
|
|
314
|
+
const fb = f(b);
|
|
315
|
+
if (fa <= 0) out.push(a);
|
|
316
|
+
if ((fa < 0 && fb > 0) || (fa > 0 && fb < 0)) {
|
|
317
|
+
const t = fa / (fa - fb);
|
|
318
|
+
out.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Celdas de Voronoi (polígonos) de unos puntos dentro de un rectángulo. O(n²), para n pequeño. */
|
|
325
|
+
export function voronoi(sites, x0, y0, x1, y1) {
|
|
326
|
+
return sites.map((p, i) => {
|
|
327
|
+
let poly = [[x0, y0], [x1, y0], [x1, y1], [x0, y1]];
|
|
328
|
+
for (let j = 0; j < sites.length && poly.length; j++) if (j !== i) poly = clipHalf(poly, p, sites[j]);
|
|
329
|
+
return poly;
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Puntos en rejilla con jitter (celdas más parejas que el azar puro). */
|
|
334
|
+
function jitterGrid(rnd, x0, y0, w, h, cols, rows, j = 0.8) {
|
|
335
|
+
const pts = [];
|
|
336
|
+
for (let r = 0; r < rows; r++) {
|
|
337
|
+
for (let c = 0; c < cols; c++) {
|
|
338
|
+
pts.push([x0 + ((c + 0.5 + (rnd() - 0.5) * j) * w) / cols, y0 + ((r + 0.5 + (rnd() - 0.5) * j) * h) / rows]);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return pts;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const polyD = (poly) => (poly.length ? `M${poly.map((p) => `${n(p[0])} ${n(p[1])}`).join('L')}Z` : '');
|
|
345
|
+
|
|
346
|
+
// ───────────────────────────── filtros ─────────────────────────────
|
|
347
|
+
|
|
348
|
+
// Región fija en espacio de usuario: cabe el cuerpo con alas, manos y efectos (y no cambia al animar).
|
|
349
|
+
const REGION = 'filterUnits="userSpaceOnUse" x="-160" y="-200" width="560" height="580" color-interpolation-filters="sRGB"';
|
|
350
|
+
|
|
351
|
+
const posterize = (levels, inn = 'SourceGraphic', res = 'p') => {
|
|
352
|
+
const tv = Array.from({ length: levels }, (_, i) => n(i / (levels - 1))).join(' ');
|
|
353
|
+
return `<feComponentTransfer in="${inn}" result="${res}"><feFuncR type="discrete" tableValues="${tv}"/><feFuncG type="discrete" tableValues="${tv}"/><feFuncB type="discrete" tableValues="${tv}"/></feComponentTransfer>`;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
/** Grano (motas de un color) recortado a la silueta → resultado `res`. */
|
|
357
|
+
const grain = (seed, rgb, k, freq = 0.9, res = 'gr') => {
|
|
358
|
+
const [r, g, b] = hexToRgb(rgb).map((v) => n(v / 255));
|
|
359
|
+
return `<feTurbulence type="fractalNoise" baseFrequency="${freq}" numOctaves="2" seed="${seed % 997}" result="${res}n"/><feColorMatrix in="${res}n" values="0 0 0 0 ${r} 0 0 0 0 ${g} 0 0 0 0 ${b} ${k} 0 0 0 ${n(-k * 0.56)}" result="${res}c"/><feComposite in="${res}c" in2="SourceAlpha" operator="in" result="${res}"/>`;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
/** Temblor de trazo a mano. */
|
|
363
|
+
const wobble = (seed, inn, scale, freq = 0.045, res = 'wb') =>
|
|
364
|
+
`<feTurbulence type="fractalNoise" baseFrequency="${freq}" numOctaves="1" seed="${(seed + 11) % 997}" result="${res}n"/><feDisplacementMap in="${inn}" in2="${res}n" scale="${scale}" xChannelSelector="R" yChannelSelector="G" result="${res}"/>`;
|
|
365
|
+
|
|
366
|
+
function filterBody(id, seed, q, animate) {
|
|
367
|
+
const full = q === 'full';
|
|
368
|
+
switch (id) {
|
|
369
|
+
case 'ukiyoe':
|
|
370
|
+
// grabado: grano de papel y borde de plancha (las tintas planas ya vienen de la paleta)
|
|
371
|
+
return full ? grain(seed, '#5a3d1e', 1.6) + wobble(seed, 'SourceGraphic', 2.6) + '<feMerge><feMergeNode in="wb"/><feMergeNode in="gr"/></feMerge>' : '';
|
|
372
|
+
case 'clay': {
|
|
373
|
+
if (!full) return '';
|
|
374
|
+
// plastilina: bultos (desplazamiento) + luz difusa y brillo sobre la silueta desenfocada
|
|
375
|
+
const light = '<feDistantLight azimuth="235" elevation="52"/>';
|
|
376
|
+
return `${wobble(seed, 'SourceGraphic', 5, 0.035, 'd')}<feGaussianBlur in="d" stdDeviation="5" result="b"/>`
|
|
377
|
+
+ `<feDiffuseLighting in="b" surfaceScale="4" diffuseConstant="1.18" lighting-color="#fff" result="dl">${light}</feDiffuseLighting>`
|
|
378
|
+
+ '<feComposite in="d" in2="dl" operator="arithmetic" k1="1" result="sh"/>'
|
|
379
|
+
+ `<feSpecularLighting in="b" surfaceScale="4" specularConstant=".5" specularExponent="20" lighting-color="#fff" result="sp">${light}</feSpecularLighting>`
|
|
380
|
+
+ '<feComposite in="sp" in2="d" operator="in" result="sp2"/><feComposite in="sh" in2="sp2" operator="arithmetic" k2="1" k3=".85"/>';
|
|
381
|
+
}
|
|
382
|
+
case 'glass':
|
|
383
|
+
// vidrio: plomo grueso alrededor, color saturado y posterizado, manchas de luz
|
|
384
|
+
return '<feMorphology in="SourceAlpha" operator="dilate" radius="2.6" result="dl"/><feFlood flood-color="#141014"/><feComposite in2="dl" operator="in" result="lead"/>'
|
|
385
|
+
+ '<feColorMatrix in="SourceGraphic" type="saturate" values="1.35" result="s"/>' + posterize(5, 's')
|
|
386
|
+
+ (full
|
|
387
|
+
? `<feTurbulence type="fractalNoise" baseFrequency=".05" numOctaves="2" seed="${seed % 997}" result="ln"/><feColorMatrix in="ln" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 .92 .9 0 0 0 -.38" result="lc"/><feComposite in="lc" in2="SourceAlpha" operator="in" result="hi"/>`
|
|
388
|
+
+ '<feMerge><feMergeNode in="lead"/><feMergeNode in="p"/><feMergeNode in="hi"/></feMerge>'
|
|
389
|
+
: '<feMerge><feMergeNode in="lead"/><feMergeNode in="p"/></feMerge>');
|
|
390
|
+
case 'bauhaus':
|
|
391
|
+
return full ? grain(seed, '#3a3226', 1.2, 1.1) + '<feMerge><feMergeNode in="SourceGraphic"/><feMergeNode in="gr"/></feMerge>' : '';
|
|
392
|
+
case 'manuscript':
|
|
393
|
+
return full ? wobble(seed, 'SourceGraphic', 1.6, 0.06) + grain(seed, '#6b4a1e', 1.3) + '<feMerge><feMergeNode in="wb"/><feMergeNode in="gr"/></feMerge>' : '';
|
|
394
|
+
case 'glitch': {
|
|
395
|
+
// píxeles: una muestra de 2×2 cada 6 unidades, dilatada hasta tapar el bloque
|
|
396
|
+
let f = '<feFlood x="-160" y="-200" width="2" height="2" flood-color="#000"/><feComposite width="6" height="6"/><feTile result="tile"/>'
|
|
397
|
+
+ '<feComposite in="SourceGraphic" in2="tile" operator="in"/><feMorphology operator="dilate" radius="2" result="px"/>';
|
|
398
|
+
let src = 'px';
|
|
399
|
+
if (full) {
|
|
400
|
+
// desgarro: bandas horizontales que se desplazan a tirones
|
|
401
|
+
const anim = animate ? '<animate attributeName="scale" values="0;0;0;0;16;0;0;0;-10;0;0;0;0;8;0" dur="3.4s" repeatCount="indefinite" calcMode="discrete"/>' : '';
|
|
402
|
+
f += `<feTurbulence type="fractalNoise" baseFrequency="0 .09" numOctaves="1" seed="${seed % 997}" result="tn"/>`
|
|
403
|
+
+ '<feComponentTransfer in="tn" result="tb"><feFuncR type="discrete" tableValues=".5 .5 .5 .1 .5 .5 .5 .95 .5 .5"/><feFuncG type="linear" slope="0" intercept=".5"/></feComponentTransfer>'
|
|
404
|
+
+ `<feDisplacementMap in="px" in2="tb" scale="${animate ? 0 : 10}" xChannelSelector="R" yChannelSelector="G" result="tear">${anim}</feDisplacementMap>`;
|
|
405
|
+
src = 'tear';
|
|
406
|
+
}
|
|
407
|
+
const dx = animate ? '<animate attributeName="dx" values="-2.5;-2.5;-2.5;-6;-2.5;-1;-2.5" dur="1.9s" repeatCount="indefinite" calcMode="discrete"/>' : '';
|
|
408
|
+
// separación RGB: rojo a un lado, verde+azul al otro
|
|
409
|
+
f += `<feColorMatrix in="${src}" values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" result="r"/><feOffset in="r" dx="-2.5" result="r2">${dx}</feOffset>`
|
|
410
|
+
+ `<feColorMatrix in="${src}" values="0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0" result="gb"/><feOffset in="gb" dx="2.5" result="gb2"/>`
|
|
411
|
+
+ '<feBlend in="r2" in2="gb2" mode="screen"/>';
|
|
412
|
+
return f;
|
|
413
|
+
}
|
|
414
|
+
case 'ink':
|
|
415
|
+
return '<feColorMatrix in="SourceGraphic" type="saturate" values="0" result="bw"/>' + (full ? wobble(seed, 'bw', 2.2, 0.05) : '');
|
|
416
|
+
case 'pop':
|
|
417
|
+
// viñeta: sombra negra dura desplazada (barata: vale también en lite)
|
|
418
|
+
return '<feFlood flood-color="#111"/><feComposite in2="SourceAlpha" operator="in"/><feOffset dx="5" dy="5" result="sh"/>'
|
|
419
|
+
+ '<feMerge><feMergeNode in="sh"/><feMergeNode in="SourceGraphic"/></feMerge>';
|
|
420
|
+
case 'watercolor':
|
|
421
|
+
if (!full) return '';
|
|
422
|
+
// borde que se corre, pigmento irregular (aguada + granulado), cerco oscuro donde se seca el agua, papel
|
|
423
|
+
return `${wobble(seed, 'SourceGraphic', 5, 0.03, 'd')}`
|
|
424
|
+
+ `<feTurbulence type="fractalNoise" baseFrequency=".018" numOctaves="3" seed="${(seed + 5) % 997}" result="pn"/><feColorMatrix in="pn" values="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1" result="pg"/>`
|
|
425
|
+
+ '<feComposite in="d" in2="pg" operator="arithmetic" k1="1.5" k2=".3" result="pig"/>'
|
|
426
|
+
+ '<feGaussianBlur in="d" stdDeviation="3" result="eb"/><feComposite in="d" in2="eb" operator="out" result="edge"/><feColorMatrix in="edge" values=".5 0 0 0 0 0 .5 0 0 0 0 0 .5 0 0 0 0 0 1 0" result="ed"/>'
|
|
427
|
+
+ grain(seed, '#3a2a40', 1.5, 0.55) + '<feMerge><feMergeNode in="pig"/><feMergeNode in="ed"/><feMergeNode in="gr"/></feMerge>'
|
|
428
|
+
+ '<feColorMatrix values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 .9 0"/>';
|
|
429
|
+
case 'pixel':
|
|
430
|
+
// bloques de 8: una muestra de 2×2 cada 8 unidades, dilatada; alfa a todo o nada (sin antialias)
|
|
431
|
+
return '<feFlood x="-160" y="-200" width="2" height="2" flood-color="#000"/><feComposite width="8" height="8"/><feTile result="tile"/>'
|
|
432
|
+
+ '<feComposite in="SourceGraphic" in2="tile" operator="in"/><feMorphology operator="dilate" radius="3"/>'
|
|
433
|
+
+ '<feComponentTransfer><feFuncA type="discrete" tableValues="0 1 1"/></feComponentTransfer>';
|
|
434
|
+
case 'synthwave':
|
|
435
|
+
if (!full) return '';
|
|
436
|
+
// tubo de neón: halo magenta alrededor de la silueta
|
|
437
|
+
return '<feGaussianBlur in="SourceAlpha" stdDeviation="5" result="ga"/><feFlood flood-color="#ff2bd6"/><feComposite in2="ga" operator="in" result="glow"/>'
|
|
438
|
+
+ '<feMerge><feMergeNode in="glow"/><feMergeNode in="glow"/><feMergeNode in="SourceGraphic"/></feMerge>';
|
|
439
|
+
case 'nouveau':
|
|
440
|
+
return full ? wobble(seed, 'SourceGraphic', 1.4, 0.05) + grain(seed, '#6b5030', 1.1) + '<feMerge><feMergeNode in="wb"/><feMergeNode in="gr"/></feMerge>' : '';
|
|
441
|
+
case 'cubism':
|
|
442
|
+
if (!full) return '';
|
|
443
|
+
// planos fracturados: ruido de baja frecuencia posterizado → cada zona se desplaza distinto
|
|
444
|
+
return `<feTurbulence type="fractalNoise" baseFrequency=".022" numOctaves="1" seed="${seed % 997}" result="cn"/>`
|
|
445
|
+
+ '<feComponentTransfer in="cn" result="cd"><feFuncR type="discrete" tableValues=".32 .62 .44 .7 .5"/><feFuncG type="discrete" tableValues=".6 .38 .52 .66 .42"/></feComponentTransfer>'
|
|
446
|
+
+ '<feDisplacementMap in="SourceGraphic" in2="cd" scale="20" xChannelSelector="R" yChannelSelector="G" result="fr"/>'
|
|
447
|
+
+ grain(seed, '#3a2e22', 1.1) + '<feMerge><feMergeNode in="fr"/><feMergeNode in="gr"/></feMerge>';
|
|
448
|
+
case 'crayon':
|
|
449
|
+
if (!full) return '';
|
|
450
|
+
// cera: trazo tembloroso y el diente del papel asomando en vetas
|
|
451
|
+
return wobble(seed, 'SourceGraphic', 4, 0.06)
|
|
452
|
+
+ `<feTurbulence type="fractalNoise" baseFrequency=".9 .22" numOctaves="2" seed="${(seed + 3) % 997}" result="wx"/>`
|
|
453
|
+
+ '<feColorMatrix in="wx" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -3 0 0 0 2.35" result="wa"/><feComposite in="wb" in2="wa" operator="in"/>';
|
|
454
|
+
case 'blueprint':
|
|
455
|
+
return full ? grain(seed, '#ffffff', 0.9, 1.2) + '<feMerge><feMergeNode in="SourceGraphic"/><feMergeNode in="gr"/></feMerge>' : '';
|
|
456
|
+
case 'pointillism':
|
|
457
|
+
if (!full) return '';
|
|
458
|
+
// puntos: una muestra cada 5 unidades, difuminada y recortada en círculo, sobre una base tenue
|
|
459
|
+
return '<feFlood x="-160" y="-200" width="2" height="2" flood-color="#000"/><feComposite width="5" height="5"/><feTile result="tile"/>'
|
|
460
|
+
+ '<feComposite in="SourceGraphic" in2="tile" operator="in"/><feGaussianBlur stdDeviation="1.15"/>'
|
|
461
|
+
+ '<feComponentTransfer result="dt"><feFuncA type="linear" slope="6" intercept="-.45"/></feComponentTransfer>'
|
|
462
|
+
+ wobble(seed, 'dt', 2.5, 0.25, 'dj')
|
|
463
|
+
+ '<feColorMatrix in="SourceGraphic" values=".8 0 0 0 .18 0 .8 0 0 .18 0 0 .8 0 .18 0 0 0 .8 0" result="un"/>'
|
|
464
|
+
+ '<feMerge><feMergeNode in="un"/><feMergeNode in="dj"/></feMerge>';
|
|
465
|
+
case 'woodcut':
|
|
466
|
+
return full ? wobble(seed, 'SourceGraphic', 2, 0.05) + grain(seed, '#efe4cc', 1.4, 0.7) + '<feMerge><feMergeNode in="wb"/><feMergeNode in="gr"/></feMerge>' : '';
|
|
467
|
+
default:
|
|
468
|
+
return '';
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ───────────────────────────── capa interior ─────────────────────────────
|
|
473
|
+
|
|
474
|
+
/** Lo que el estilo dibuja dentro del cuerpo (recortado a su contorno por quien llama). */
|
|
475
|
+
function innerLayer(s, L, uid, d) {
|
|
476
|
+
const sx = s._style;
|
|
477
|
+
const b = L.box;
|
|
478
|
+
const rnd = seeded(sx.seed ^ 0x5f3759df);
|
|
479
|
+
let defs = '';
|
|
480
|
+
let svg = '';
|
|
481
|
+
switch (sx.id) {
|
|
482
|
+
case 'ukiyoe': {
|
|
483
|
+
// bokashi: degradado de tinta arriba y vetas de la plancha de madera
|
|
484
|
+
defs += `<linearGradient id="${uid}-bk" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1b2233" stop-opacity=".32"/><stop offset=".4" stop-color="#1b2233" stop-opacity="0"/></linearGradient>`;
|
|
485
|
+
svg += `<rect x="${n(b.x - 4)}" y="${n(b.y - 4)}" width="${n(b.w + 8)}" height="${n(b.h + 8)}" fill="url(#${uid}-bk)"/>`;
|
|
486
|
+
let grainD = '';
|
|
487
|
+
for (let i = 0; i < 9; i++) {
|
|
488
|
+
const y = b.y + b.h * (0.12 + i * 0.1 + rnd() * 0.04);
|
|
489
|
+
const a = 2 + rnd() * 3;
|
|
490
|
+
grainD += `M${n(b.x - 4)} ${n(y)}C${n(b.x + b.w * 0.3)} ${n(y - a)} ${n(b.x + b.w * 0.6)} ${n(y + a)} ${n(b.x + b.w + 4)} ${n(y)}`;
|
|
491
|
+
}
|
|
492
|
+
svg += `<path d="${grainD}" fill="none" stroke="#1b2233" stroke-width=".8" opacity=".14"/>`;
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
case 'clay': {
|
|
496
|
+
// huellas: medias lunas de dedo apretado en la masa
|
|
497
|
+
for (let i = 0; i < 4; i++) {
|
|
498
|
+
const x = b.x + b.w * (0.2 + rnd() * 0.6);
|
|
499
|
+
const y = b.y + b.h * (0.25 + rnd() * 0.6);
|
|
500
|
+
const r = b.w * (0.05 + rnd() * 0.04);
|
|
501
|
+
svg += `<path d="M${n(x - r)} ${n(y)}A${n(r)} ${n(r * 0.6)} 0 0 0 ${n(x + r)} ${n(y)}" fill="none" stroke="#000" stroke-width="1.4" stroke-linecap="round" opacity=".1"/>`;
|
|
502
|
+
}
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
case 'glass': {
|
|
506
|
+
// cristales: celdas de Voronoi con tono variable y plomo oscuro
|
|
507
|
+
const sites = jitterGrid(rnd, b.x, b.y, b.w, b.h, 3, 4, 0.9);
|
|
508
|
+
const cells = voronoi(sites, b.x - 10, b.y - 10, b.x + b.w + 10, b.y + b.h + 10);
|
|
509
|
+
let lead = '';
|
|
510
|
+
cells.forEach((poly, i) => {
|
|
511
|
+
const p = polyD(poly);
|
|
512
|
+
const k = rnd();
|
|
513
|
+
svg += `<path d="${p}" fill="${k < 0.5 ? '#000' : '#fff'}" opacity="${n(0.06 + (i % 3) * 0.06)}"/>`;
|
|
514
|
+
lead += p;
|
|
515
|
+
});
|
|
516
|
+
svg += `<path d="${lead}" fill="none" stroke="#141014" stroke-width="3.2" stroke-linejoin="round"/>`;
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
case 'bauhaus': {
|
|
520
|
+
// mitades de primarios y un círculo: el cuerpo se vuelve composición
|
|
521
|
+
const c2 = s.body.patternColor;
|
|
522
|
+
const c3 = PAL.bauhaus.find((c) => c !== s.body.color && c !== c2) || '#151515';
|
|
523
|
+
const right = rnd() < 0.5;
|
|
524
|
+
svg += `<rect x="${n(right ? L.cx : b.x - 6)}" y="${n(b.y - 6)}" width="${n(b.w / 2 + 6)}" height="${n(b.h + 12)}" fill="${c2}"/>`;
|
|
525
|
+
svg += `<circle cx="${n(right ? b.x + b.w * 0.18 : b.x + b.w * 0.82)}" cy="${n(b.y + b.h * 0.92)}" r="${n(b.w * 0.26)}" fill="${c3}"/>`;
|
|
526
|
+
svg += `<path d="M${n(L.cx)} ${n(b.y - 6)}V${n(b.y + b.h + 6)}" stroke="#151515" stroke-width="3"/>`;
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
case 'manuscript': {
|
|
530
|
+
// pan de oro: punteado y filete dorado por dentro del contorno
|
|
531
|
+
defs += `<pattern id="${uid}-gd" width="9" height="9" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><circle cx="4.5" cy="4.5" r="1.1" fill="#c99a2e"/></pattern>`;
|
|
532
|
+
svg += `<rect x="${n(b.x - 4)}" y="${n(b.y - 4)}" width="${n(b.w + 8)}" height="${n(b.h + 8)}" fill="url(#${uid}-gd)" opacity=".55"/>`;
|
|
533
|
+
svg += `<path d="${d}" fill="none" stroke="#c99a2e" stroke-width="7"/><path d="${d}" fill="none" stroke="#f3d27a" stroke-width="2" opacity=".8"/>`;
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
case 'glitch': {
|
|
537
|
+
// líneas de barrido
|
|
538
|
+
defs += `<pattern id="${uid}-sl" width="4" height="4" patternUnits="userSpaceOnUse"><rect width="4" height="1.4" fill="#000" opacity=".28"/></pattern>`;
|
|
539
|
+
svg += `<rect x="${n(b.x - 4)}" y="${n(b.y - 4)}" width="${n(b.w + 8)}" height="${n(b.h + 8)}" fill="url(#${uid}-sl)"/>`;
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
case 'ink': {
|
|
543
|
+
// sombreado cruzado suave en la parte de sombra
|
|
544
|
+
defs += `<pattern id="${uid}-xh" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(-35)"><path d="M0 0V6" stroke="#222" stroke-width=".8"/></pattern>`;
|
|
545
|
+
svg += `<ellipse cx="${n(L.cx + b.w * 0.32)}" cy="${n(b.y + b.h * 0.78)}" rx="${n(b.w * 0.42)}" ry="${n(b.h * 0.36)}" fill="url(#${uid}-xh)" opacity=".35"/>`;
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
case 'pop': {
|
|
549
|
+
// Ben-Day: trama de puntos claros en todo el cuerpo, oscuros en la sombra, y un brillo de cómic
|
|
550
|
+
defs += `<pattern id="${uid}-bl" width="7" height="7" patternUnits="userSpaceOnUse" patternTransform="rotate(30)"><circle cx="3.5" cy="3.5" r="2" fill="#fff" opacity=".5"/></pattern>`;
|
|
551
|
+
defs += `<pattern id="${uid}-bk" width="7" height="7" patternUnits="userSpaceOnUse" patternTransform="rotate(30)"><circle cx="3.5" cy="3.5" r="2.4" fill="#111" opacity=".42"/></pattern>`;
|
|
552
|
+
svg += `<rect x="${n(b.x - 4)}" y="${n(b.y - 4)}" width="${n(b.w + 8)}" height="${n(b.h + 8)}" fill="url(#${uid}-bl)"/>`;
|
|
553
|
+
svg += `<path d="${d}" fill="url(#${uid}-bk)" transform="translate(${n(b.w * 0.16)} ${n(b.h * 0.14)})"/>`;
|
|
554
|
+
const gx = b.x + b.w * 0.24;
|
|
555
|
+
const gy = b.y + b.h * 0.18;
|
|
556
|
+
svg += `<path d="M${n(gx)} ${n(gy + 14)}Q${n(gx + 2)} ${n(gy)} ${n(gx + 16)} ${n(gy - 4)}" fill="none" stroke="#fff" stroke-width="5" stroke-linecap="round"/><circle cx="${n(gx + 24)}" cy="${n(gy - 6)}" r="2.6" fill="#fff"/>`;
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
case 'watercolor': {
|
|
560
|
+
// floraciones: manchas con el cerco más oscuro, y un hueco de papel sin pintar como brillo
|
|
561
|
+
defs += `<radialGradient id="${uid}-wb"><stop offset="0" stop-color="#000" stop-opacity="0"/><stop offset=".7" stop-color="#000" stop-opacity=".04"/><stop offset=".93" stop-color="#000" stop-opacity=".16"/><stop offset="1" stop-color="#000" stop-opacity="0"/></radialGradient>`;
|
|
562
|
+
defs += `<radialGradient id="${uid}-wh"><stop offset="0" stop-color="#fffdf6" stop-opacity=".85"/><stop offset=".6" stop-color="#fffdf6" stop-opacity=".5"/><stop offset="1" stop-color="#fffdf6" stop-opacity="0"/></radialGradient>`;
|
|
563
|
+
for (let i = 0; i < 5; i++) {
|
|
564
|
+
svg += `<circle cx="${n(b.x + b.w * (0.15 + rnd() * 0.7))}" cy="${n(b.y + b.h * (0.3 + rnd() * 0.65))}" r="${n(b.w * (0.12 + rnd() * 0.14))}" fill="url(#${uid}-wb)"/>`;
|
|
565
|
+
}
|
|
566
|
+
svg += `<ellipse cx="${n(b.x + b.w * 0.3)}" cy="${n(b.y + b.h * 0.22)}" rx="${n(b.w * 0.16)}" ry="${n(b.h * 0.1)}" fill="url(#${uid}-wh)"/>`;
|
|
567
|
+
svg += `<path d="${d}" fill="#000" opacity=".1" transform="translate(${n(b.w * 0.12)} ${n(b.h * 0.16)})"/>`;
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
case 'pixel': {
|
|
571
|
+
// sombreado de dos tonos (los píxeles del filtro lo escalonan)
|
|
572
|
+
svg += `<path d="${d}" fill="#000" opacity=".26" transform="translate(${n(b.w * 0.14)} ${n(b.h * 0.14)})"/>`;
|
|
573
|
+
svg += `<rect x="${n(b.x + b.w * 0.18)}" y="${n(b.y + b.h * 0.1)}" width="${n(b.w * 0.16)}" height="${n(b.h * 0.08)}" fill="#fff" opacity=".55"/>`;
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
case 'synthwave': {
|
|
577
|
+
// atardecer en el cuerpo: violeta abajo, franjas cromadas y líneas de barrido
|
|
578
|
+
defs += `<linearGradient id="${uid}-sw" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#fff" stop-opacity=".28"/><stop offset=".35" stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#2a0857" stop-opacity=".6"/></linearGradient>`;
|
|
579
|
+
defs += `<pattern id="${uid}-sl" width="4" height="4" patternUnits="userSpaceOnUse"><rect width="4" height="1" fill="#1a0b33" opacity=".22"/></pattern>`;
|
|
580
|
+
svg += `<rect x="${n(b.x - 4)}" y="${n(b.y - 4)}" width="${n(b.w + 8)}" height="${n(b.h + 8)}" fill="url(#${uid}-sw)"/><rect x="${n(b.x - 4)}" y="${n(b.y - 4)}" width="${n(b.w + 8)}" height="${n(b.h + 8)}" fill="url(#${uid}-sl)"/>`;
|
|
581
|
+
let bars = '';
|
|
582
|
+
for (let i = 0; i < 4; i++) bars += `<rect x="${n(b.x - 4)}" y="${n(b.y + b.h * (0.62 + i * 0.09))}" width="${n(b.w + 8)}" height="${n(1.5 + i * 1.3)}"/>`;
|
|
583
|
+
svg += `<g fill="#1a0b33" opacity=".45">${bars}</g>`;
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
case 'nouveau': {
|
|
587
|
+
// latigazos dorados que suben como tallos, con capullos
|
|
588
|
+
let curves = '';
|
|
589
|
+
let buds = '';
|
|
590
|
+
for (let i = 0; i < 3; i++) {
|
|
591
|
+
const x = b.x + b.w * (0.2 + i * 0.3 + (rnd() - 0.5) * 0.08);
|
|
592
|
+
const y0 = b.y + b.h + 4;
|
|
593
|
+
const y1 = b.y + b.h * (0.5 + rnd() * 0.15);
|
|
594
|
+
const sw = (i % 2 ? 1 : -1) * b.w * 0.12;
|
|
595
|
+
curves += `M${n(x)} ${n(y0)}C${n(x + sw)} ${n(y0 - (y0 - y1) * 0.4)} ${n(x - sw)} ${n(y1 + (y0 - y1) * 0.3)} ${n(x + sw * 0.4)} ${n(y1)}`;
|
|
596
|
+
buds += `<circle cx="${n(x + sw * 0.4)}" cy="${n(y1 - 3)}" r="3.2"/>`;
|
|
597
|
+
}
|
|
598
|
+
svg += `<path d="${curves}" fill="none" stroke="#c9a24a" stroke-width="2.4" stroke-linecap="round" opacity=".8"/><g fill="#c9a24a" opacity=".85">${buds}</g>`;
|
|
599
|
+
svg += `<path d="${d}" fill="none" stroke="#efe2c4" stroke-width="10" opacity=".25"/>`;
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
case 'cubism': {
|
|
603
|
+
// facetas: celdas de Voronoi muy irregulares, cada una con su propia luz y algún plano de color
|
|
604
|
+
defs += `<linearGradient id="${uid}-c0" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fff" stop-opacity=".35"/><stop offset="1" stop-color="#000" stop-opacity=".3"/></linearGradient>`;
|
|
605
|
+
defs += `<linearGradient id="${uid}-c1" x1="1" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#000" stop-opacity=".35"/><stop offset="1" stop-color="#fff" stop-opacity=".25"/></linearGradient>`;
|
|
606
|
+
const sites = jitterGrid(rnd, b.x, b.y, b.w, b.h, 3, 3, 1.1);
|
|
607
|
+
const cells = voronoi(sites, b.x - 10, b.y - 10, b.x + b.w + 10, b.y + b.h + 10);
|
|
608
|
+
let lines = '';
|
|
609
|
+
cells.forEach((poly, i) => {
|
|
610
|
+
const p = polyD(poly);
|
|
611
|
+
const k = rnd();
|
|
612
|
+
if (k < 0.38) svg += `<path d="${p}" fill="${PAL.cubism[Math.floor(rnd() * 5)]}" opacity=".65"/>`;
|
|
613
|
+
svg += `<path d="${p}" fill="url(#${uid}-c${i % 2})"/>`;
|
|
614
|
+
lines += p;
|
|
615
|
+
});
|
|
616
|
+
svg += `<path d="${lines}" fill="none" stroke="#2e2a26" stroke-width="1.6" stroke-linejoin="miter" opacity=".75"/>`;
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
case 'crayon': {
|
|
620
|
+
// relleno de niño: zigzag de cera que se sale de las líneas
|
|
621
|
+
let z = '';
|
|
622
|
+
const step = 5.5;
|
|
623
|
+
let up = true;
|
|
624
|
+
for (let x = b.x - 10; x < b.x + b.w + 10; x += step) {
|
|
625
|
+
z += `${z ? 'L' : 'M'}${n(x + rnd() * 2)} ${n(up ? b.y - 6 + rnd() * 8 : b.y + b.h + 6 - rnd() * 8)}`;
|
|
626
|
+
up = !up;
|
|
627
|
+
}
|
|
628
|
+
svg += `<path d="${z}" fill="none" stroke="#000" stroke-width="2.2" stroke-linejoin="round" opacity=".14" transform="rotate(14 ${n(L.cx)} ${n(b.y + b.h / 2)})"/>`;
|
|
629
|
+
svg += `<path d="${d}" fill="none" stroke="#fff" stroke-width="7" opacity=".35"/>`;
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
case 'blueprint': {
|
|
633
|
+
// cuadrícula fina y ejes de construcción (raya-punto) con un círculo guía
|
|
634
|
+
defs += `<pattern id="${uid}-bg" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M10 0H0V10" fill="none" stroke="#eaf2ff" stroke-width=".5" opacity=".35"/></pattern>`;
|
|
635
|
+
svg += `<rect x="${n(b.x - 4)}" y="${n(b.y - 4)}" width="${n(b.w + 8)}" height="${n(b.h + 8)}" fill="url(#${uid}-bg)"/>`;
|
|
636
|
+
const cy = b.y + b.h * 0.55;
|
|
637
|
+
svg += `<path d="M${n(L.cx)} ${n(b.y - 6)}V${n(b.y + b.h + 6)}M${n(b.x - 6)} ${n(cy)}H${n(b.x + b.w + 6)}" stroke="#eaf2ff" stroke-width=".9" stroke-dasharray="9 3 2 3" opacity=".75"/>`;
|
|
638
|
+
svg += `<circle cx="${n(L.cx)}" cy="${n(cy)}" r="${n(Math.min(b.w, b.h) * 0.36)}" fill="none" stroke="#eaf2ff" stroke-width=".8" stroke-dasharray="4 3" opacity=".55"/>`;
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
case 'pointillism': {
|
|
642
|
+
// toques de color: claros, complementarios y oscuros (en lite es lo único que queda del estilo)
|
|
643
|
+
const [h] = toHsl(s.body.color);
|
|
644
|
+
const comp = fromHsl((h + 180) % 360, 0.6, 0.6);
|
|
645
|
+
const ana = fromHsl((h + 40) % 360, 0.7, 0.62);
|
|
646
|
+
defs += `<pattern id="${uid}-pt" width="9" height="9" patternUnits="userSpaceOnUse"><circle cx="1.5" cy="2" r="1.4" fill="#fff" opacity=".6"/><circle cx="6" cy="1.5" r="1.3" fill="${comp}" opacity=".7"/><circle cx="4" cy="6" r="1.4" fill="${ana}" opacity=".8"/><circle cx="8" cy="7" r="1.2" fill="#000" opacity=".22"/><circle cx="1" cy="7.5" r="1.1" fill="${comp}" opacity=".45"/></pattern>`;
|
|
647
|
+
svg += `<rect x="${n(b.x - 4)}" y="${n(b.y - 4)}" width="${n(b.w + 8)}" height="${n(b.h + 8)}" fill="url(#${uid}-pt)"/>`;
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
case 'woodcut': {
|
|
651
|
+
// tallas: el contorno repetido hacia dentro (líneas de gubia) y un sombreado de rayas
|
|
652
|
+
const cx = L.cx;
|
|
653
|
+
const cy = b.y + b.h * 0.58;
|
|
654
|
+
let rings = '';
|
|
655
|
+
[0.84, 0.7, 0.57, 0.45].forEach((k, i) => {
|
|
656
|
+
rings += `<path d="${d}" transform="translate(${n(cx * (1 - k))} ${n(cy * (1 - k))}) scale(${k})" stroke-width="${n((2.6 - i * 0.45) / k)}"/>`;
|
|
657
|
+
});
|
|
658
|
+
svg += `<g fill="none" stroke="#15120f" opacity=".8">${rings}</g>`;
|
|
659
|
+
defs += `<pattern id="${uid}-wh" width="5" height="5" patternUnits="userSpaceOnUse" patternTransform="rotate(-40)"><rect width="5" height="2.2" fill="#15120f"/></pattern>`;
|
|
660
|
+
svg += `<path d="${d}" fill="url(#${uid}-wh)" transform="translate(${n(b.w * 0.22)} ${n(b.h * 0.2)})"/>`;
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return { defs, svg };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Piezas del estilo para renderLayers: defs (filtro + patrones), capa interior del cuerpo y el
|
|
669
|
+
* `url(#…)` del filtro que se aplica a todo el personaje ('' si no hay).
|
|
670
|
+
* @param {object} s spec ya estilizada (stylizeSpec)
|
|
671
|
+
* @param {object} L layout del cuerpo
|
|
672
|
+
* @param {{ uid:string, d:string, quality?:'full'|'lite'|'off', animate?:boolean }} ctx
|
|
673
|
+
*/
|
|
674
|
+
export function styleLayers(s, L, ctx) {
|
|
675
|
+
const sx = s?._style;
|
|
676
|
+
if (!sx) return { defs: '', inner: '', filter: '' };
|
|
677
|
+
const q = ctx.quality === 'off' ? 'lite' : ctx.quality || 'full';
|
|
678
|
+
const fb = filterBody(sx.id, sx.seed, q, !!ctx.animate);
|
|
679
|
+
const il = innerLayer(s, L, ctx.uid, ctx.d);
|
|
680
|
+
const fid = `${ctx.uid}-sty`;
|
|
681
|
+
return {
|
|
682
|
+
defs: il.defs + (fb ? `<filter id="${fid}" ${REGION}>${fb}</filter>` : ''),
|
|
683
|
+
inner: il.svg,
|
|
684
|
+
filter: fb ? `url(#${fid})` : '',
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ───────────────────────────── fondos ─────────────────────────────
|
|
689
|
+
|
|
690
|
+
// Marco fijo de 300×300; el suelo arranca hacia y=240 (los pies del ot caen ahí).
|
|
691
|
+
const W = 300;
|
|
692
|
+
const GROUND = 240;
|
|
693
|
+
|
|
694
|
+
const BACKDROPS = {
|
|
695
|
+
ukiyoe(p, rnd) {
|
|
696
|
+
const sunX = 190 + rnd() * 50;
|
|
697
|
+
let s = `<defs><filter id="${p}g" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency=".75" numOctaves="2" seed="${Math.floor(rnd() * 900)}"/><feColorMatrix values="0 0 0 0 .35 0 0 0 0 .25 0 0 0 0 .12 0 0 0 1.4 -.72"/></filter></defs>`;
|
|
698
|
+
s += `<rect width="${W}" height="${W}" fill="#ece0c4"/>`;
|
|
699
|
+
s += `<circle cx="${n(sunX)}" cy="62" r="34" fill="#c8402c"/>`;
|
|
700
|
+
// Fuji con su capa de nieve dentada
|
|
701
|
+
const fx = 150 + rnd() * 30;
|
|
702
|
+
s += `<path d="M${n(fx - 95)} ${GROUND}L${n(fx - 16)} 128Q${n(fx)} 118 ${n(fx + 16)} 128L${n(fx + 95)} ${GROUND}Z" fill="#46688a"/>`;
|
|
703
|
+
let snow = `M${n(fx - 16)} 128Q${n(fx)} 118 ${n(fx + 16)} 128L${n(fx + 32)} 152`;
|
|
704
|
+
for (let i = 0; i < 6; i++) snow += `L${n(fx + 32 - (i + 0.5) * 10.7)} ${n(i % 2 ? 150 : 140 + rnd() * 6)}`;
|
|
705
|
+
s += `<path d="${snow}L${n(fx - 32)} 152Z" fill="#f3ead6"/>`;
|
|
706
|
+
// mar con escamas de olas
|
|
707
|
+
s += `<rect y="214" width="${W}" height="${GROUND - 214}" fill="#2f5579"/>`;
|
|
708
|
+
let sc = '';
|
|
709
|
+
for (let y = 218; y < GROUND; y += 7) for (let x = (y % 14 ? 0 : 7); x < W; x += 14) sc += `M${x} ${y + 5}a7 7 0 0 1 14 0`;
|
|
710
|
+
s += `<path d="${sc}" fill="none" stroke="#e9e0c8" stroke-width="1.1" opacity=".75"/>`;
|
|
711
|
+
// la gran ola: lengua oscura, banda clara y garras de espuma
|
|
712
|
+
s += '<path d="M0 240V150C8 92 70 56 124 78C146 88 152 108 140 122C132 102 106 98 96 116C88 132 100 152 124 162C96 178 70 204 78 240Z" fill="#1f3a5f"/>';
|
|
713
|
+
s += '<path d="M8 236C4 170 40 112 92 100" fill="none" stroke="#5d86a8" stroke-width="9" opacity=".8"/><path d="M22 236C22 186 46 140 84 124" fill="none" stroke="#e9e0c8" stroke-width="1.4" opacity=".6"/><path d="M36 236C38 196 56 160 86 146" fill="none" stroke="#e9e0c8" stroke-width="1.4" opacity=".5"/>';
|
|
714
|
+
let claws = '';
|
|
715
|
+
const crest = [[124, 78], [134, 86], [141, 98], [142, 110], [140, 122], [128, 108], [116, 102], [104, 104]];
|
|
716
|
+
for (const [x, y] of crest) claws += `<circle cx="${x}" cy="${y}" r="${n(3 + rnd() * 2.5)}" fill="#f3ead6"/>`;
|
|
717
|
+
for (let i = 0; i < 14; i++) claws += `<circle cx="${n(100 + rnd() * 70)}" cy="${n(60 + rnd() * 60)}" r="${n(0.8 + rnd() * 1.6)}" fill="#f3ead6"/>`;
|
|
718
|
+
s += claws;
|
|
719
|
+
// suelo de madera: tablas, juntas y vetas
|
|
720
|
+
s += `<rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="#b98a55"/>`;
|
|
721
|
+
let pl = '';
|
|
722
|
+
for (const y of [254, 270, 287]) {
|
|
723
|
+
pl += `M0 ${y}H${W}`;
|
|
724
|
+
for (let x = rnd() * 60; x < W; x += 70 + rnd() * 50) pl += `M${n(x)} ${y}v${y === 287 ? 13 : y === 254 ? -14 : -16}`;
|
|
725
|
+
}
|
|
726
|
+
s += `<path d="M0 ${GROUND}H${W}" stroke="#6e4826" stroke-width="2"/><path d="${pl}" stroke="#8a5f34" stroke-width="1.2" fill="none"/>`;
|
|
727
|
+
// sello vertical rojo con 七つ
|
|
728
|
+
s += '<rect x="262" y="22" width="24" height="62" rx="2" fill="#b8322a"/><text x="274" y="48" font-size="19" text-anchor="middle" fill="#f3e6cc" font-family="\'Noto Serif CJK JP\',\'Hiragino Mincho ProN\',serif">七</text><text x="274" y="74" font-size="17" text-anchor="middle" fill="#f3e6cc" font-family="\'Noto Serif CJK JP\',\'Hiragino Mincho ProN\',serif">つ</text>';
|
|
729
|
+
s += `<rect width="${W}" height="${W}" filter="url(#${p}g)"/>`;
|
|
730
|
+
return s;
|
|
731
|
+
},
|
|
732
|
+
|
|
733
|
+
clay(p, rnd) {
|
|
734
|
+
const light = '<feDistantLight azimuth="235" elevation="50"/>';
|
|
735
|
+
let s = `<defs><linearGradient id="${p}sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#8fcdf0"/><stop offset="1" stop-color="#dff3fb"/></linearGradient>`
|
|
736
|
+
+ `<filter id="${p}c" x="-10%" y="-10%" width="120%" height="120%"><feTurbulence type="fractalNoise" baseFrequency=".03" numOctaves="2" seed="${Math.floor(rnd() * 900)}"/><feDisplacementMap in="SourceGraphic" scale="6" xChannelSelector="R" yChannelSelector="G" result="d"/><feGaussianBlur in="d" stdDeviation="4" result="b"/><feDiffuseLighting in="b" surfaceScale="4" diffuseConstant="1.18" lighting-color="#fff" result="l">${light}</feDiffuseLighting><feComposite in="d" in2="l" operator="arithmetic" k1="1" result="sh"/><feSpecularLighting in="b" surfaceScale="4" specularConstant=".45" specularExponent="18" lighting-color="#fff" result="sp">${light}</feSpecularLighting><feComposite in="sp" in2="d" operator="in" result="sp2"/><feComposite in="sh" in2="sp2" operator="arithmetic" k2="1" k3=".8"/></filter></defs>`;
|
|
737
|
+
s += `<rect width="${W}" height="${W}" fill="url(#${p}sky)"/>`;
|
|
738
|
+
const F = `filter="url(#${p}c)"`;
|
|
739
|
+
// sol con rayos
|
|
740
|
+
const sx = 228 + rnd() * 30;
|
|
741
|
+
let rays = '';
|
|
742
|
+
for (let i = 0; i < 12; i++) rays += `<rect x="-4" y="-50" width="8" height="16" rx="4" fill="#ffb52e" transform="translate(${n(sx)} 58) rotate(${i * 30 + rnd() * 6})"/>`;
|
|
743
|
+
s += `<g ${F}>${rays}<circle cx="${n(sx)}" cy="58" r="26" fill="#ffcf3f"/></g>`;
|
|
744
|
+
// nubes gorditas
|
|
745
|
+
for (let k = 0; k < 2; k++) {
|
|
746
|
+
const cx = 40 + k * 110 + rnd() * 30;
|
|
747
|
+
const cy = 40 + rnd() * 40;
|
|
748
|
+
s += `<g ${F} fill="#fbfbff"><circle cx="${n(cx)}" cy="${n(cy)}" r="16"/><circle cx="${n(cx + 18)}" cy="${n(cy - 8)}" r="20"/><circle cx="${n(cx + 38)}" cy="${n(cy)}" r="15"/><rect x="${n(cx)}" y="${n(cy)}" width="38" height="15" rx="7"/></g>`;
|
|
749
|
+
}
|
|
750
|
+
// colinas
|
|
751
|
+
s += `<g ${F}><ellipse cx="60" cy="${GROUND + 8}" rx="120" ry="52" fill="#86c96a"/><ellipse cx="250" cy="${GROUND + 10}" rx="110" ry="44" fill="#9bd27a"/></g>`;
|
|
752
|
+
// árbol robusto
|
|
753
|
+
const tx = 40 + rnd() * 20;
|
|
754
|
+
s += `<g ${F}><path d="M${n(tx - 9)} ${GROUND + 4}C${n(tx - 6)} 200 ${n(tx - 8)} 180 ${n(tx - 4)} 150H${n(tx + 6)}C${n(tx + 9)} 180 ${n(tx + 8)} 200 ${n(tx + 12)} ${GROUND + 4}Z" fill="#8a5a33"/></g>`;
|
|
755
|
+
s += `<g ${F} fill="#4fae4a"><circle cx="${n(tx)}" cy="128" r="30"/><circle cx="${n(tx - 24)}" cy="148" r="20"/><circle cx="${n(tx + 24)}" cy="146" r="22"/><circle cx="${n(tx + 6)}" cy="104" r="20" fill="#5cc254"/></g>`;
|
|
756
|
+
// tierra con borde grumoso y piedritas
|
|
757
|
+
let top = `M-10 ${W}V${GROUND}`;
|
|
758
|
+
for (let x = -10; x < W + 10; x += 22) top += `Q${n(x + 11)} ${n(GROUND - 5 - rnd() * 6)} ${x + 22} ${GROUND}`;
|
|
759
|
+
s += `<g ${F}><path d="${top}V${W}Z" fill="#a8703f"/></g>`;
|
|
760
|
+
let peb = '';
|
|
761
|
+
for (let i = 0; i < 7; i++) peb += `<ellipse cx="${n(rnd() * W)}" cy="${n(GROUND + 14 + rnd() * 50)}" rx="${n(4 + rnd() * 5)}" ry="${n(3 + rnd() * 2)}" fill="${rnd() < 0.5 ? '#8a5730' : '#c08a58'}"/>`;
|
|
762
|
+
s += `<g ${F}>${peb}</g>`;
|
|
763
|
+
return s;
|
|
764
|
+
},
|
|
765
|
+
|
|
766
|
+
glass(p, rnd) {
|
|
767
|
+
const arch = `M40 ${GROUND}V118A140 140 0 0 1 150 12A140 140 0 0 1 260 118V${GROUND}Z`;
|
|
768
|
+
let s = `<defs><clipPath id="${p}a"><path d="${arch}"/></clipPath><radialGradient id="${p}gl" cx=".5" cy=".35" r=".6"><stop offset="0" stop-color="#fff" stop-opacity=".35"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></radialGradient></defs>`;
|
|
769
|
+
s += `<rect width="${W}" height="${W}" fill="#1c1622"/>`;
|
|
770
|
+
// sillares de piedra
|
|
771
|
+
let st = '';
|
|
772
|
+
for (let y = 0; y < GROUND; y += 20) for (let x = (y / 20) % 2 ? -15 : 0; x < W; x += 30) st += `M${x} ${y}h30v20`;
|
|
773
|
+
s += `<path d="${st}" fill="none" stroke="#2c2434" stroke-width="1.5"/>`;
|
|
774
|
+
// ventana: cristales de Voronoi con plomo
|
|
775
|
+
const sites = jitterGrid(rnd, 40, 12, 220, GROUND - 12, 6, 8, 0.95);
|
|
776
|
+
const cells = voronoi(sites, 40, 0, 260, GROUND);
|
|
777
|
+
let glass = '';
|
|
778
|
+
let lead = '';
|
|
779
|
+
cells.forEach((poly, i) => {
|
|
780
|
+
const y = sites[i][1] / GROUND;
|
|
781
|
+
// más azules arriba, más cálidos abajo
|
|
782
|
+
const pool = y < 0.45 ? ['#1e5aa8', '#2aa0b8', '#6a2c8c', '#1e5aa8', '#e2a52a'] : ['#b8222a', '#e2a52a', '#2f8a4a', '#d65a1f', '#1e5aa8'];
|
|
783
|
+
const d = polyD(poly);
|
|
784
|
+
glass += `<path d="${d}" fill="${pool[Math.floor(rnd() * pool.length)]}"/>`;
|
|
785
|
+
lead += d;
|
|
786
|
+
});
|
|
787
|
+
s += `<g clip-path="url(#${p}a)">${glass}<rect width="${W}" height="${W}" fill="url(#${p}gl)"/><path d="${lead}" fill="none" stroke="#141014" stroke-width="3.4" stroke-linejoin="round"/><path d="M150 12V${GROUND}M40 128H260" stroke="#141014" stroke-width="5"/><circle cx="150" cy="80" r="30" fill="none" stroke="#141014" stroke-width="5"/></g>`;
|
|
788
|
+
s += `<path d="${arch}" fill="none" stroke="#3a3044" stroke-width="10"/><path d="${arch}" fill="none" stroke="#141014" stroke-width="4"/>`;
|
|
789
|
+
// suelo de piedra con charcos de luz de colores
|
|
790
|
+
s += `<rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="#2a2230"/><rect y="${GROUND}" width="${W}" height="6" fill="#3a3044"/>`;
|
|
791
|
+
const pools = ['#1e5aa8', '#b8222a', '#e2a52a', '#2f8a4a'];
|
|
792
|
+
for (let i = 0; i < 4; i++) s += `<ellipse cx="${n(70 + i * 55 + rnd() * 15)}" cy="${n(GROUND + 26 + rnd() * 20)}" rx="${n(20 + rnd() * 12)}" ry="7" fill="${pools[i]}" opacity=".35"/>`;
|
|
793
|
+
return s;
|
|
794
|
+
},
|
|
795
|
+
|
|
796
|
+
bauhaus(p, rnd) {
|
|
797
|
+
let s = `<rect width="${W}" height="${W}" fill="#efe7d4"/>`;
|
|
798
|
+
// tira de película arriba
|
|
799
|
+
s += `<rect y="10" width="${W}" height="46" fill="#151515"/>`;
|
|
800
|
+
let holes = '';
|
|
801
|
+
for (let x = 4; x < W; x += 14) holes += `<rect x="${x}" y="14" width="7" height="5" rx="1" fill="#efe7d4"/><rect x="${x}" y="47" width="7" height="5" rx="1" fill="#efe7d4"/>`;
|
|
802
|
+
s += holes;
|
|
803
|
+
const icons = [
|
|
804
|
+
(x) => `<circle cx="${x + 30}" cy="35" r="9" fill="#d7262b"/>`,
|
|
805
|
+
(x) => `<rect x="${x + 21}" y="26" width="18" height="18" fill="#1f4e9c"/>`,
|
|
806
|
+
(x) => `<path d="M${x + 20} 44L${x + 30} 26L${x + 40} 44Z" fill="#f2b705"/>`,
|
|
807
|
+
];
|
|
808
|
+
for (let i = 0; i < 5; i++) {
|
|
809
|
+
const x = -20 + i * 68 + (rnd() * 6);
|
|
810
|
+
s += `<rect x="${n(x)}" y="22" width="60" height="25" fill="#2b2b2b"/>${icons[(i + Math.floor(rnd() * 3)) % 3](n(x))}`;
|
|
811
|
+
}
|
|
812
|
+
// composición: círculo rojo, rectángulo azul, triángulo amarillo, barras negras
|
|
813
|
+
const left = rnd() < 0.5;
|
|
814
|
+
s += `<circle cx="${left ? 92 : 208}" cy="148" r="64" fill="#d7262b"/>`;
|
|
815
|
+
s += `<rect x="${left ? 196 : 26}" y="78" width="78" height="${GROUND - 78}" fill="#1f4e9c"/>`;
|
|
816
|
+
s += `<path d="M${left ? 170 : 40} ${GROUND}L${left ? 290 : 160} ${GROUND}L${left ? 230 : 100} 132Z" fill="#f2b705"/>`;
|
|
817
|
+
s += `<rect x="0" y="${n(96 + rnd() * 20)}" width="${W}" height="7" fill="#151515" transform="rotate(${left ? -14 : 14} 150 120)"/>`;
|
|
818
|
+
s += '<text x="28" y="230" transform="rotate(-90 28 230)" font-family="Futura,\'Century Gothic\',\'Avenir Next\',\'Helvetica Neue\',Arial,sans-serif" font-weight="900" font-size="30" letter-spacing="2" fill="#151515">BAUHAUS</text>';
|
|
819
|
+
s += `<text x="292" y="${GROUND - 8}" text-anchor="end" font-family="Futura,'Century Gothic','Helvetica Neue',Arial,sans-serif" font-weight="900" font-size="26" fill="#151515">1923</text>`;
|
|
820
|
+
// suelo
|
|
821
|
+
s += `<rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="#e2d6bb"/><rect y="${GROUND}" width="${W}" height="5" fill="#151515"/><rect x="${left ? 220 : 20}" y="${GROUND + 20}" width="60" height="10" fill="#1f4e9c"/>`;
|
|
822
|
+
return s;
|
|
823
|
+
},
|
|
824
|
+
|
|
825
|
+
manuscript(p, rnd) {
|
|
826
|
+
const letters = 'OASMTE';
|
|
827
|
+
const L = letters[Math.floor(rnd() * letters.length)];
|
|
828
|
+
let s = `<defs><filter id="${p}g" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency=".6" numOctaves="3" seed="${Math.floor(rnd() * 900)}"/><feColorMatrix values="0 0 0 0 .45 0 0 0 0 .32 0 0 0 0 .14 0 0 0 1.2 -.62"/></filter></defs>`;
|
|
829
|
+
s += `<rect width="${W}" height="${W}" fill="#e9d6aa"/>`;
|
|
830
|
+
// escena en mosaico de teselas (cielo, estrellas doradas, colinas)
|
|
831
|
+
const x0 = 24;
|
|
832
|
+
const y0 = 84;
|
|
833
|
+
const T = 9;
|
|
834
|
+
let mos = '';
|
|
835
|
+
const hill = (x) => 196 + Math.sin(x / 38 + rnd() * 0.02) * 14 + Math.sin(x / 17) * 5;
|
|
836
|
+
for (let y = y0; y < 276; y += T) {
|
|
837
|
+
for (let x = x0; x < 276; x += T) {
|
|
838
|
+
let c;
|
|
839
|
+
if (y + T > 268) c = ['#7a4a26', '#8a5a30', '#6e4220'][Math.floor(rnd() * 3)];
|
|
840
|
+
else if (y > hill(x)) c = ['#2e6b3a', '#3a7d44', '#25592f'][Math.floor(rnd() * 3)];
|
|
841
|
+
else {
|
|
842
|
+
const k = (y - y0) / 120;
|
|
843
|
+
c = rnd() < 0.05 && k < 0.7 ? '#e0b64a' : k < 0.35 ? ['#1d3f8f', '#22489d', '#193780'][Math.floor(rnd() * 3)] : ['#3a64b0', '#4672bb', '#3159a3'][Math.floor(rnd() * 3)];
|
|
844
|
+
}
|
|
845
|
+
mos += `<rect x="${x + 0.6}" y="${y + 0.6}" width="${T - 1.2}" height="${T - 1.2}" fill="${c}"/>`;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
s += `<rect x="${x0 - 2}" y="${y0 - 2}" width="${276 - x0 + 4}" height="${276 - y0 + 2}" fill="#3b2412"/>${mos}`;
|
|
849
|
+
s += `<rect x="${x0 - 3}" y="${y0 - 3}" width="${276 - x0 + 6}" height="${276 - y0 + 4}" fill="none" stroke="#c99a2e" stroke-width="3"/>`;
|
|
850
|
+
// inicial capitular dorada
|
|
851
|
+
s += '<rect x="22" y="20" width="52" height="54" fill="#1d3f8f" stroke="#c99a2e" stroke-width="3"/><path d="M26 70C40 56 30 38 46 30" fill="none" stroke="#2e6b3a" stroke-width="2"/><circle cx="46" cy="30" r="3" fill="#b3261e"/>';
|
|
852
|
+
s += `<text x="48" y="64" text-anchor="middle" font-family="'Cinzel','Trajan Pro','Book Antiqua',Palatino,Georgia,serif" font-weight="700" font-size="44" fill="#e0b64a" stroke="#7a5a16" stroke-width=".8">${L}</text>`;
|
|
853
|
+
// dos columnas de texto (renglones de «palabras»), la primera con rúbrica roja
|
|
854
|
+
let txt = '';
|
|
855
|
+
for (const [cx0, cx1] of [[84, 176], [186, 278]]) {
|
|
856
|
+
for (let r = 0; r < 6; r++) {
|
|
857
|
+
let x = cx0;
|
|
858
|
+
const y = 24 + r * 9;
|
|
859
|
+
while (x < cx1 - 6) {
|
|
860
|
+
const w = Math.min(cx1 - x, 6 + rnd() * 16);
|
|
861
|
+
txt += `<rect x="${n(x)}" y="${y}" width="${n(w)}" height="2.6" rx="1" fill="${r === 0 && x === cx0 ? '#b3261e' : '#3b2412'}" opacity=".78"/>`;
|
|
862
|
+
x += w + 3;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
s += txt;
|
|
867
|
+
// marco: oro, filete rojo y medallones en las esquinas
|
|
868
|
+
s += `<rect x="6" y="6" width="${W - 12}" height="${W - 12}" fill="none" stroke="#c99a2e" stroke-width="6"/><rect x="13" y="13" width="${W - 26}" height="${W - 26}" fill="none" stroke="#b3261e" stroke-width="1.5"/>`;
|
|
869
|
+
for (const [x, y] of [[9, 9], [291, 9], [9, 291], [291, 291]]) s += `<circle cx="${x}" cy="${y}" r="8" fill="#c99a2e"/><circle cx="${x}" cy="${y}" r="3.5" fill="#b3261e"/>`;
|
|
870
|
+
s += `<rect width="${W}" height="${W}" filter="url(#${p}g)"/>`;
|
|
871
|
+
return s;
|
|
872
|
+
},
|
|
873
|
+
|
|
874
|
+
glitch(p, rnd) {
|
|
875
|
+
// degradado tramado: cada banda se cubre con 25/50/75 % de píxeles del color siguiente
|
|
876
|
+
const cols = ['#120a2a', '#2a0f55', '#5a1478', '#a21a8a', '#ff2bd6'];
|
|
877
|
+
const dens = [[[0, 0]], [[0, 0], [3, 3]], [[0, 0], [3, 3], [3, 0]]];
|
|
878
|
+
let s = '<defs>';
|
|
879
|
+
for (let i = 1; i < cols.length; i++) {
|
|
880
|
+
dens.forEach((pts, k) => {
|
|
881
|
+
s += `<pattern id="${p}d${i}${k}" width="6" height="6" patternUnits="userSpaceOnUse">${pts.map(([x, y]) => `<rect x="${x}" y="${y}" width="3" height="3" fill="${cols[i]}"/>`).join('')}</pattern>`;
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
s += `<pattern id="${p}sl" width="3" height="3" patternUnits="userSpaceOnUse"><rect width="3" height="1.2" fill="#000" opacity=".35"/></pattern></defs>`;
|
|
885
|
+
const band = GROUND / (cols.length - 1);
|
|
886
|
+
for (let i = 0; i < cols.length - 1; i++) {
|
|
887
|
+
const y = i * band;
|
|
888
|
+
s += `<rect y="${n(y)}" width="${W}" height="${n(band + 1)}" fill="${cols[i]}"/>`;
|
|
889
|
+
for (let k = 0; k < 3; k++) s += `<rect y="${n(y + (k * band) / 3)}" width="${W}" height="${n(band / 3 + 0.5)}" fill="url(#${p}d${i + 1}${k})"/>`;
|
|
890
|
+
}
|
|
891
|
+
// suelo: rejilla en perspectiva con aberración cromática
|
|
892
|
+
s += `<rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="#0b0b14"/>`;
|
|
893
|
+
let grid = '';
|
|
894
|
+
for (let i = 0; i <= 12; i++) grid += `M${150 + (i - 6) * 9} ${GROUND}L${150 + (i - 6) * 70} ${W}`;
|
|
895
|
+
for (let j = 1; j < 6; j++) {
|
|
896
|
+
const y = GROUND + (W - GROUND) * (j / 6) ** 1.7;
|
|
897
|
+
grid += `M0 ${n(y)}H${W}`;
|
|
898
|
+
}
|
|
899
|
+
s += `<path d="${grid}" stroke="#ff2b5e" stroke-width="1" fill="none" opacity=".7" transform="translate(-1.5 0)"/><path d="${grid}" stroke="#2bf3ff" stroke-width="1" fill="none" opacity=".8" transform="translate(1.5 0)"/><path d="M0 ${GROUND}H${W}" stroke="#fff" stroke-width="1.5"/>`;
|
|
900
|
+
// bloques de error
|
|
901
|
+
let blocks = '';
|
|
902
|
+
const bc = ['#2bf3ff', '#ff2bd6', '#ffffff', '#f2ff2b'];
|
|
903
|
+
for (let i = 0; i < 9; i++) blocks += `<rect x="${n(rnd() * W - 30)}" y="${n(rnd() * GROUND)}" width="${n(20 + rnd() * 90)}" height="${n(2 + rnd() * 7)}" fill="${bc[Math.floor(rnd() * bc.length)]}" opacity="${n(0.25 + rnd() * 0.5)}"/>`;
|
|
904
|
+
s += blocks;
|
|
905
|
+
const ty = 40 + rnd() * 30;
|
|
906
|
+
const font = 'font-family="\'Courier New\',ui-monospace,monospace" font-weight="700" font-size="22" letter-spacing="3"';
|
|
907
|
+
s += `<text x="148" y="${n(ty)}" ${font} text-anchor="middle" fill="#ff2b5e" opacity=".85">7OTS.EXE</text><text x="152" y="${n(ty)}" ${font} text-anchor="middle" fill="#2bf3ff" opacity=".85">7OTS.EXE</text><text x="150" y="${n(ty)}" ${font} text-anchor="middle" fill="#fff">7OTS.EXE</text>`;
|
|
908
|
+
s += `<rect width="${W}" height="${W}" fill="url(#${p}sl)"/>`;
|
|
909
|
+
return s;
|
|
910
|
+
},
|
|
911
|
+
|
|
912
|
+
ink(p, rnd) {
|
|
913
|
+
let s = `<defs><filter id="${p}w" x="-5%" y="-5%" width="110%" height="110%"><feTurbulence type="fractalNoise" baseFrequency=".04" numOctaves="2" seed="${Math.floor(rnd() * 900)}"/><feDisplacementMap in="SourceGraphic" scale="3" xChannelSelector="R" yChannelSelector="G"/></filter><filter id="${p}g" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency=".9" numOctaves="2" seed="3"/><feColorMatrix values="0 0 0 0 .4 0 0 0 0 .38 0 0 0 0 .34 0 0 0 .9 -.52"/></filter></defs>`;
|
|
914
|
+
s += `<rect width="${W}" height="${W}" fill="#f7f3ea"/>`;
|
|
915
|
+
// montañas: dos crestas a línea, la lejana más tenue
|
|
916
|
+
const ridge = (base, amp, k) => {
|
|
917
|
+
let d = `M-10 ${base}`;
|
|
918
|
+
for (let x = 0; x <= W + 10; x += 18 + rnd() * 14) d += `L${n(x)} ${n(base - amp * (0.3 + rnd() * 0.7) * (0.6 + 0.4 * Math.sin(x / (40 + k * 20))))}`;
|
|
919
|
+
return d;
|
|
920
|
+
};
|
|
921
|
+
let lines = `<path d="${ridge(170, 70, 1)}" stroke="#b4ada0" stroke-width="1.2"/><path d="${ridge(205, 55, 2)}" stroke="#7d776c" stroke-width="1.6"/>`;
|
|
922
|
+
// sol a línea y pájaros
|
|
923
|
+
lines += `<circle cx="${n(215 + rnd() * 40)}" cy="${n(55 + rnd() * 15)}" r="20" stroke="#8d877c" stroke-width="1.3"/>`;
|
|
924
|
+
for (let i = 0; i < 3; i++) {
|
|
925
|
+
const x = 60 + rnd() * 120;
|
|
926
|
+
const y = 40 + rnd() * 40;
|
|
927
|
+
lines += `<path d="M${n(x - 6)} ${n(y)}q3 -4 6 0q3 -4 6 0" stroke="#5c574f" stroke-width="1.1"/>`;
|
|
928
|
+
}
|
|
929
|
+
// ola ligera
|
|
930
|
+
lines += '<path d="M-5 238C20 206 60 192 92 204C110 211 112 228 98 232C90 220 76 222 76 232" stroke="#6f6a61" stroke-width="1.6"/><path d="M110 236C140 226 170 228 196 238M200 232C230 222 262 224 300 236" stroke="#9a948a" stroke-width="1.2"/>';
|
|
931
|
+
// suelo: una línea y unos trazos de sombreado
|
|
932
|
+
lines += `<path d="M0 ${GROUND + 2}H${W}" stroke="#3b3833" stroke-width="1.8"/>`;
|
|
933
|
+
let hatch = '';
|
|
934
|
+
for (let i = 0; i < 22; i++) {
|
|
935
|
+
const x = rnd() * W;
|
|
936
|
+
const y = GROUND + 10 + rnd() * 50;
|
|
937
|
+
hatch += `M${n(x)} ${n(y)}l${n(10 + rnd() * 14)} -3`;
|
|
938
|
+
}
|
|
939
|
+
lines += `<path d="${hatch}" stroke="#8d877c" stroke-width="1"/>`;
|
|
940
|
+
s += `<g fill="none" stroke-linecap="round" stroke-linejoin="round" filter="url(#${p}w)">${lines}</g>`;
|
|
941
|
+
s += `<rect width="${W}" height="${W}" filter="url(#${p}g)"/>`;
|
|
942
|
+
return s;
|
|
943
|
+
},
|
|
944
|
+
|
|
945
|
+
pop(p, rnd) {
|
|
946
|
+
const word = ['POW!', 'WOW!', 'ZAP!', 'BAM!', 'WHAAM!'][Math.floor(rnd() * 5)];
|
|
947
|
+
let s = `<defs><pattern id="${p}d" width="9" height="9" patternUnits="userSpaceOnUse" patternTransform="rotate(30)"><circle cx="4.5" cy="4.5" r="2.6" fill="#e4002b" opacity=".55"/></pattern>`
|
|
948
|
+
+ `<pattern id="${p}w" width="8" height="8" patternUnits="userSpaceOnUse" patternTransform="rotate(30)"><circle cx="4" cy="4" r="2.2" fill="#fff" opacity=".8"/></pattern></defs>`;
|
|
949
|
+
// rayos amarillos alternos desde detrás del ot
|
|
950
|
+
s += `<rect width="${W}" height="${W}" fill="#ffe14a"/>`;
|
|
951
|
+
let rays = '';
|
|
952
|
+
const cx = 150;
|
|
953
|
+
const cy = 150;
|
|
954
|
+
for (let i = 0; i < 16; i++) {
|
|
955
|
+
const a0 = (i / 16) * Math.PI * 2;
|
|
956
|
+
const a1 = a0 + Math.PI / 16;
|
|
957
|
+
rays += `M${cx} ${cy}L${n(cx + Math.cos(a0) * 320)} ${n(cy + Math.sin(a0) * 320)}L${n(cx + Math.cos(a1) * 320)} ${n(cy + Math.sin(a1) * 320)}Z`;
|
|
958
|
+
}
|
|
959
|
+
s += `<path d="${rays}" fill="#ffd400"/><rect width="${W}" height="${W}" fill="url(#${p}d)" opacity=".7"/>`;
|
|
960
|
+
// estallido con la onomatopeya
|
|
961
|
+
const bx = 68 + rnd() * 12;
|
|
962
|
+
const by = 66;
|
|
963
|
+
let burst = '';
|
|
964
|
+
for (let i = 0; i < 26; i++) {
|
|
965
|
+
const a = (i / 26) * Math.PI * 2;
|
|
966
|
+
const r = i % 2 ? 30 : 56 + rnd() * 10;
|
|
967
|
+
burst += `${i ? 'L' : 'M'}${n(bx + Math.cos(a) * r * 1.25)} ${n(by + Math.sin(a) * r)}`;
|
|
968
|
+
}
|
|
969
|
+
s += `<path d="${burst}Z" fill="#e4002b" stroke="#111" stroke-width="4" stroke-linejoin="miter"/>`;
|
|
970
|
+
s += `<path d="${burst}Z" fill="#fff36b" transform="translate(${n(bx * 0.38)} ${n(by * 0.38)}) scale(.62)"/>`;
|
|
971
|
+
const fs = word.length > 4 ? 22 : 28;
|
|
972
|
+
s += `<text x="${n(bx)}" y="${n(by + fs * 0.36)}" text-anchor="middle" transform="rotate(-10 ${n(bx)} ${n(by)})" font-family="Impact,'Arial Black','Helvetica Neue',sans-serif" font-weight="900" font-size="${fs}" fill="#0072ce" stroke="#111" stroke-width="1.6" paint-order="stroke">${word}</text>`;
|
|
973
|
+
// globo de diálogo arriba a la derecha
|
|
974
|
+
s += '<path d="M196 22H284Q292 22 292 30V58Q292 66 284 66H236L222 80L224 66H196Q188 66 188 58V30Q188 22 196 22Z" fill="#fff" stroke="#111" stroke-width="3"/>';
|
|
975
|
+
s += '<text x="240" y="40" text-anchor="middle" font-family="\'Comic Sans MS\',\'Comic Neue\',\'Chalkboard SE\',sans-serif" font-weight="700" font-size="12" fill="#111">I\'M AN OT,</text><text x="240" y="56" text-anchor="middle" font-family="\'Comic Sans MS\',\'Comic Neue\',\'Chalkboard SE\',sans-serif" font-weight="700" font-size="12" fill="#111">DARLING!</text>';
|
|
976
|
+
// suelo azul con trama blanca, y el marco de la viñeta
|
|
977
|
+
s += `<rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="#0072ce"/><rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="url(#${p}w)" opacity=".5"/><path d="M0 ${GROUND}H${W}" stroke="#111" stroke-width="4"/>`;
|
|
978
|
+
s += `<rect x="3" y="3" width="${W - 6}" height="${W - 6}" fill="none" stroke="#111" stroke-width="6"/>`;
|
|
979
|
+
return s;
|
|
980
|
+
},
|
|
981
|
+
|
|
982
|
+
watercolor(p, rnd) {
|
|
983
|
+
const sd = () => Math.floor(rnd() * 900);
|
|
984
|
+
let s = `<defs><filter id="${p}w" x="-15%" y="-15%" width="130%" height="130%"><feTurbulence type="fractalNoise" baseFrequency=".022" numOctaves="3" seed="${sd()}"/><feDisplacementMap in="SourceGraphic" scale="16" xChannelSelector="R" yChannelSelector="G" result="d"/><feGaussianBlur in="d" stdDeviation="1.1" result="b"/><feGaussianBlur in="d" stdDeviation="3.5" result="eb"/><feComposite in="d" in2="eb" operator="out" result="e"/><feColorMatrix in="e" values=".6 0 0 0 0 0 .6 0 0 0 0 0 .6 0 0 0 0 0 .5 0" result="ed"/><feMerge><feMergeNode in="b"/><feMergeNode in="ed"/></feMerge></filter>`
|
|
985
|
+
+ `<filter id="${p}g" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency=".7" numOctaves="3" seed="${sd()}"/><feColorMatrix values="0 0 0 0 .45 0 0 0 0 .38 0 0 0 0 .3 0 0 0 1.1 -.6"/></filter>`
|
|
986
|
+
+ `<linearGradient id="${p}sky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#6fa3d6"/><stop offset=".7" stop-color="#a9cbe8"/><stop offset="1" stop-color="#a9cbe8" stop-opacity="0"/></linearGradient></defs>`;
|
|
987
|
+
s += `<rect width="${W}" height="${W}" fill="#fbf7ee"/>`;
|
|
988
|
+
const F = `filter="url(#${p}w)"`;
|
|
989
|
+
// aguadas: cielo, nubes sin pintar, sol corrido
|
|
990
|
+
s += `<g ${F}><rect x="10" y="10" width="280" height="170" fill="url(#${p}sky)" opacity=".85"/></g>`;
|
|
991
|
+
for (let k = 0; k < 3; k++) {
|
|
992
|
+
const x = 30 + k * 90 + rnd() * 30;
|
|
993
|
+
const y = 40 + rnd() * 50;
|
|
994
|
+
s += `<g ${F} fill="#fbf7ee"><ellipse cx="${n(x)}" cy="${n(y)}" rx="${n(22 + rnd() * 12)}" ry="11"/><ellipse cx="${n(x + 16)}" cy="${n(y - 8)}" rx="16" ry="12"/></g>`;
|
|
995
|
+
}
|
|
996
|
+
s += `<g ${F}><circle cx="${n(220 + rnd() * 40)}" cy="${n(54 + rnd() * 16)}" r="22" fill="#f4b860" opacity=".8"/></g>`;
|
|
997
|
+
// colinas en capas (cada una más cálida y opaca)
|
|
998
|
+
const hill = (base, amp, col, op) => {
|
|
999
|
+
let d = `M0 ${W}V${base}`;
|
|
1000
|
+
for (let x = 0; x <= W; x += 30) d += `Q${x + 15} ${n(base - amp * (0.4 + rnd() * 0.6))} ${x + 30} ${n(base - rnd() * amp * 0.3)}`;
|
|
1001
|
+
return `<g ${F}><path d="${d}V${W}Z" fill="${col}" opacity="${op}"/></g>`;
|
|
1002
|
+
};
|
|
1003
|
+
s += hill(170, 40, '#7d8fc8', 0.7) + hill(205, 34, '#5fae7a', 0.72) + hill(GROUND, 18, '#a4c75a', 0.8);
|
|
1004
|
+
s += `<g ${F}><rect x="10" y="${GROUND + 4}" width="280" height="${W - GROUND - 14}" fill="#c49a66" opacity=".65"/></g>`;
|
|
1005
|
+
// salpicaduras
|
|
1006
|
+
const spl = ['#d35d6e', '#6fa3d6', '#e7a33e', '#7fb38a'];
|
|
1007
|
+
let sp = '';
|
|
1008
|
+
for (let i = 0; i < 12; i++) sp += `<circle cx="${n(rnd() * W)}" cy="${n(rnd() * W)}" r="${n(0.8 + rnd() * 2.6)}" fill="${spl[i % 4]}" opacity=".55"/>`;
|
|
1009
|
+
s += sp;
|
|
1010
|
+
// papel: grano y margen sin pintar
|
|
1011
|
+
s += `<rect width="${W}" height="${W}" filter="url(#${p}g)" opacity=".7"/>`;
|
|
1012
|
+
s += `<g ${F}><path d="M-20 -20H320V320H-20ZM12 12V288H288V12Z" fill="#fbf7ee" fill-rule="evenodd"/></g>`;
|
|
1013
|
+
return s;
|
|
1014
|
+
},
|
|
1015
|
+
|
|
1016
|
+
pixel(p, rnd) {
|
|
1017
|
+
// sprites de texto: cada carácter es un color, '.' es transparente (se agrupan tiras horizontales)
|
|
1018
|
+
const sprite = (rows, x0, y0, k, cols) => {
|
|
1019
|
+
let o = '';
|
|
1020
|
+
rows.forEach((row, y) => {
|
|
1021
|
+
for (let x = 0; x < row.length;) {
|
|
1022
|
+
const ch = row[x];
|
|
1023
|
+
let e = x + 1;
|
|
1024
|
+
while (e < row.length && row[e] === ch) e++;
|
|
1025
|
+
if (cols[ch]) o += `<rect x="${n(x0 + x * k)}" y="${n(y0 + y * k)}" width="${(e - x) * k}" height="${k}" fill="${cols[ch]}"/>`;
|
|
1026
|
+
x = e;
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
return o;
|
|
1030
|
+
};
|
|
1031
|
+
let s = `<rect width="${W}" height="${W}" fill="#5c94fc"/>`;
|
|
1032
|
+
const cloud = ['.....wwww.......', '...wwwwwwww.....', '..wwwwwwwwwwww..', '.wwwwwwwwwwwwww.', 'wwwwwwwwwwwwwwww', 'wwwbwwwwwwwbwwww', '.wwbbbwwwwbbbww.', '..bbbbbbbbbbbb..'];
|
|
1033
|
+
for (let i = 0; i < 2; i++) s += sprite(cloud, Math.round((20 + i * 150 + rnd() * 40) / 4) * 4, 28 + Math.round(rnd() * 6) * 4, 4, { w: '#fcfcfc', b: '#3cbcfc' });
|
|
1034
|
+
// colina escalonada con su contorno
|
|
1035
|
+
const hx = 40 + Math.round(rnd() * 8) * 5;
|
|
1036
|
+
let hill = '';
|
|
1037
|
+
for (let x = -60; x <= 60; x += 6) {
|
|
1038
|
+
const h = Math.round(Math.sqrt(Math.max(0, 1 - (x / 64) ** 2)) * 70 / 6) * 6;
|
|
1039
|
+
hill += `<rect x="${hx + x}" y="${GROUND - h}" width="6" height="${h}" fill="#00a800"/><rect x="${hx + x}" y="${GROUND - h}" width="6" height="3" fill="#005800"/>`;
|
|
1040
|
+
}
|
|
1041
|
+
s += hill + `<rect x="${hx - 14}" y="${GROUND - 40}" width="4" height="10" fill="#005800"/><rect x="${hx + 10}" y="${GROUND - 50}" width="4" height="10" fill="#005800"/>`;
|
|
1042
|
+
// ladrillos y bloque «?»
|
|
1043
|
+
const qx = 170 + Math.round(rnd() * 4) * 20;
|
|
1044
|
+
const brick = (x, y) => `<rect x="${x}" y="${y}" width="20" height="20" fill="#c84c0c"/><path d="M${x} ${y + 1}H${x + 20}M${x} ${y + 10}H${x + 20}M${x + 10} ${y + 1}V${y + 10}M${x + 4} ${y + 10}V${y + 20}M${x + 16} ${y + 10}V${y + 20}" stroke="#000" stroke-width="2"/>`;
|
|
1045
|
+
s += brick(qx - 20, 150) + brick(qx + 20, 150);
|
|
1046
|
+
const q = ['..wwww..', '.ww..ww.', '.....ww.', '....ww..', '...ww...', '...ww...', '........', '...ww...'];
|
|
1047
|
+
s += `<rect x="${qx}" y="150" width="20" height="20" fill="#f8b800"/><rect x="${qx}" y="150" width="20" height="20" fill="none" stroke="#000" stroke-width="2"/>${sprite(q, qx + 2, 152, 2, { w: '#a81000' })}`;
|
|
1048
|
+
s += `<rect x="${qx + 1}" y="151" width="2" height="2" fill="#000"/><rect x="${qx + 17}" y="151" width="2" height="2" fill="#000"/><rect x="${qx + 1}" y="167" width="2" height="2" fill="#000"/><rect x="${qx + 17}" y="167" width="2" height="2" fill="#000"/>`;
|
|
1049
|
+
// moneda
|
|
1050
|
+
const coin = ['.yy.', 'yowy', 'yowy', 'yowy', 'yowy', '.yy.'];
|
|
1051
|
+
s += sprite(coin, qx + 6, 126, 2, { y: '#f8b800', o: '#ac7c00', w: '#fcfcfc' });
|
|
1052
|
+
// tubería verde
|
|
1053
|
+
const px = qx > 200 ? 18 : 246;
|
|
1054
|
+
s += `<rect x="${px + 4}" y="${GROUND - 34}" width="28" height="34" fill="#00a800"/><rect x="${px + 8}" y="${GROUND - 34}" width="5" height="34" fill="#58d854"/><rect x="${px + 26}" y="${GROUND - 34}" width="4" height="34" fill="#005800"/>`;
|
|
1055
|
+
s += `<rect x="${px}" y="${GROUND - 48}" width="36" height="14" fill="#00a800" stroke="#000" stroke-width="2"/><rect x="${px + 4}" y="${GROUND - 46}" width="5" height="10" fill="#58d854"/>`;
|
|
1056
|
+
// suelo de bloques
|
|
1057
|
+
let g = '';
|
|
1058
|
+
for (let y = GROUND; y < W; y += 20) {
|
|
1059
|
+
for (let x = 0; x < W; x += 20) {
|
|
1060
|
+
g += `<rect x="${x}" y="${y}" width="20" height="20" fill="#c84c0c"/><path d="M${x} ${y + 19}V${y}H${x + 19}" fill="none" stroke="#fcbcb0" stroke-width="2"/><path d="M${x + 1} ${y + 19}H${x + 19}V${y + 1}" fill="none" stroke="#000" stroke-width="2"/>`;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
s += g;
|
|
1064
|
+
// marcador
|
|
1065
|
+
const font = 'font-family="\'Press Start 2P\',\'Courier New\',ui-monospace,monospace" font-weight="700" font-size="11" fill="#fcfcfc"';
|
|
1066
|
+
s += `<text x="14" y="18" ${font}>OT×07</text><text x="150" y="18" text-anchor="middle" ${font}>${String(Math.floor(rnd() * 90 + 10) * 100).padStart(6, '0')}</text><text x="286" y="18" text-anchor="end" ${font}>WORLD 1-7</text>`;
|
|
1067
|
+
return s;
|
|
1068
|
+
},
|
|
1069
|
+
|
|
1070
|
+
synthwave(p, rnd) {
|
|
1071
|
+
const HZ = 182;
|
|
1072
|
+
let s = `<defs><linearGradient id="${p}sky" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="0" y2="${HZ}"><stop offset="0" stop-color="#0d0221"/><stop offset=".55" stop-color="#3a0f6e"/><stop offset="1" stop-color="#c2187a"/></linearGradient>`
|
|
1073
|
+
+ `<linearGradient id="${p}sun" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffe12b"/><stop offset=".5" stop-color="#ff8a2b"/><stop offset="1" stop-color="#ff2b8a"/></linearGradient>`
|
|
1074
|
+
+ `<linearGradient id="${p}chr" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#e8f6ff"/><stop offset=".48" stop-color="#7fb8ff"/><stop offset=".52" stop-color="#2a1050"/><stop offset="1" stop-color="#ff7ae0"/></linearGradient>`
|
|
1075
|
+
+ `<filter id="${p}gl" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="2.2" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs>`;
|
|
1076
|
+
s += `<rect width="${W}" height="${HZ}" fill="url(#${p}sky)"/>`;
|
|
1077
|
+
let st = '';
|
|
1078
|
+
for (let i = 0; i < 34; i++) st += `<circle cx="${n(rnd() * W)}" cy="${n(rnd() * HZ * 0.6)}" r="${n(0.4 + rnd() * 1.1)}"/>`;
|
|
1079
|
+
s += `<g fill="#fff" opacity=".85">${st}</g>`;
|
|
1080
|
+
// sol retro con franjas recortadas (rellenas con el mismo cielo)
|
|
1081
|
+
const sx = 150;
|
|
1082
|
+
s += `<circle cx="${sx}" cy="${HZ - 30}" r="66" fill="url(#${p}sun)"/>`;
|
|
1083
|
+
let bars = '';
|
|
1084
|
+
for (let i = 0; i < 6; i++) bars += `<rect x="${sx - 70}" y="${n(HZ - 52 + i * 9.5)}" width="140" height="${n(1.5 + i * 1.1)}"/>`;
|
|
1085
|
+
s += `<g fill="url(#${p}sky)">${bars}</g>`;
|
|
1086
|
+
// montañas de alambre a los lados
|
|
1087
|
+
const mtn = (x0, x1, top) => {
|
|
1088
|
+
let d = `M${x0} ${HZ}`;
|
|
1089
|
+
const k = 6;
|
|
1090
|
+
for (let i = 1; i < k; i++) d += `L${n(x0 + ((x1 - x0) * i) / k)} ${n(HZ - (i % 2 ? top * (0.6 + rnd() * 0.4) : top * (0.2 + rnd() * 0.3)))}`;
|
|
1091
|
+
return `${d}L${x1} ${HZ}Z`;
|
|
1092
|
+
};
|
|
1093
|
+
const mt = mtn(-10, 110, 60) + mtn(190, 310, 54);
|
|
1094
|
+
s += `<path d="${mt}" fill="#1a0838" stroke="#2bf3ff" stroke-width="1.3" stroke-linejoin="round" filter="url(#${p}gl)"/>`;
|
|
1095
|
+
// suelo: rejilla magenta en perspectiva
|
|
1096
|
+
s += `<rect y="${HZ}" width="${W}" height="${W - HZ}" fill="#12002a"/>`;
|
|
1097
|
+
let grid = '';
|
|
1098
|
+
for (let i = -10; i <= 10; i++) grid += `M${150 + i * 14} ${HZ}L${150 + i * 90} ${W}`;
|
|
1099
|
+
for (let j = 1; j < 9; j++) grid += `M0 ${n(HZ + (W - HZ) * (j / 9) ** 1.8)}H${W}`;
|
|
1100
|
+
s += `<path d="${grid}" stroke="#ff2bd6" stroke-width="1.2" fill="none" filter="url(#${p}gl)"/><path d="M0 ${HZ}H${W}" stroke="#ffb3f2" stroke-width="2" filter="url(#${p}gl)"/>`;
|
|
1101
|
+
// palmeras en silueta
|
|
1102
|
+
const palm = (x, h, flip) => {
|
|
1103
|
+
const top = HZ + 6 - h;
|
|
1104
|
+
const lean = flip ? -14 : 14;
|
|
1105
|
+
let o = `<path d="M${x} ${HZ + 6}Q${x + lean * 0.3} ${n(top + h * 0.5)} ${x + lean} ${top}" fill="none" stroke="#0a0014" stroke-width="5" stroke-linecap="round"/>`;
|
|
1106
|
+
for (let i = 0; i < 6; i++) {
|
|
1107
|
+
const a = -Math.PI + (i / 5) * Math.PI + (rnd() - 0.5) * 0.3;
|
|
1108
|
+
const ex = x + lean + Math.cos(a) * 34;
|
|
1109
|
+
const ey = top + Math.sin(a) * 18 + 16;
|
|
1110
|
+
o += `<path d="M${x + lean} ${top}Q${n((x + lean + ex) / 2)} ${n(top - 12)} ${n(ex)} ${n(ey)}" fill="none" stroke="#0a0014" stroke-width="4" stroke-linecap="round"/>`;
|
|
1111
|
+
}
|
|
1112
|
+
return o;
|
|
1113
|
+
};
|
|
1114
|
+
s += palm(26, 120, false) + palm(278, 104, true);
|
|
1115
|
+
// rótulo cromado
|
|
1116
|
+
s += `<text x="150" y="40" text-anchor="middle" font-family="'Brush Script MT','Segoe Script',cursive" font-style="italic" font-weight="700" font-size="30" fill="#ff2bd6" filter="url(#${p}gl)" transform="rotate(-6 150 36)">Seven</text>`;
|
|
1117
|
+
s += `<text x="150" y="68" text-anchor="middle" font-family="Impact,'Arial Black',sans-serif" font-style="italic" font-size="26" letter-spacing="4" fill="url(#${p}chr)" stroke="#fff" stroke-width=".6">OTS·1987</text>`;
|
|
1118
|
+
return s;
|
|
1119
|
+
},
|
|
1120
|
+
|
|
1121
|
+
nouveau(p, rnd) {
|
|
1122
|
+
let s = `<defs><filter id="${p}g" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency=".65" numOctaves="2" seed="${Math.floor(rnd() * 900)}"/><feColorMatrix values="0 0 0 0 .4 0 0 0 0 .3 0 0 0 0 .18 0 0 0 1 -.6"/></filter></defs>`;
|
|
1123
|
+
s += `<rect width="${W}" height="${W}" fill="#e9dbb8"/>`;
|
|
1124
|
+
// panel arqueado salvia
|
|
1125
|
+
const arch = `M30 ${GROUND}V120A120 120 0 0 1 270 120V${GROUND}Z`;
|
|
1126
|
+
s += `<path d="${arch}" fill="#a8b48c"/>`;
|
|
1127
|
+
// halo de Mucha: anillos dorados, cuentas y radios
|
|
1128
|
+
const hx = 150;
|
|
1129
|
+
const hy = 104;
|
|
1130
|
+
s += `<circle cx="${hx}" cy="${hy}" r="98" fill="#d9b98a"/><circle cx="${hx}" cy="${hy}" r="88" fill="none" stroke="#c9a24a" stroke-width="4"/><circle cx="${hx}" cy="${hy}" r="74" fill="#efe2c4"/>`;
|
|
1131
|
+
let sp = '';
|
|
1132
|
+
for (let i = 0; i < 36; i++) {
|
|
1133
|
+
const a = (i / 36) * Math.PI * 2;
|
|
1134
|
+
sp += `M${n(hx + Math.cos(a) * 20)} ${n(hy + Math.sin(a) * 20)}L${n(hx + Math.cos(a) * 72)} ${n(hy + Math.sin(a) * 72)}`;
|
|
1135
|
+
}
|
|
1136
|
+
s += `<path d="${sp}" stroke="#c9a24a" stroke-width="1" opacity=".45"/>`;
|
|
1137
|
+
let beads = '';
|
|
1138
|
+
for (let i = 0; i < 28; i++) {
|
|
1139
|
+
const a = (i / 28) * Math.PI * 2;
|
|
1140
|
+
beads += `<circle cx="${n(hx + Math.cos(a) * 81)}" cy="${n(hy + Math.sin(a) * 81)}" r="${i % 2 ? 2.2 : 3.6}" fill="${i % 2 ? '#6e4a5e' : '#c9a24a'}"/>`;
|
|
1141
|
+
}
|
|
1142
|
+
s += beads + `<circle cx="${hx}" cy="${hy}" r="74" fill="none" stroke="#4a3324" stroke-width="1.5"/>`;
|
|
1143
|
+
// tallos en latigazo a ambos lados, con hojas y flores
|
|
1144
|
+
const vine = (sx) => {
|
|
1145
|
+
const m = sx < 150 ? 1 : -1;
|
|
1146
|
+
const x = sx;
|
|
1147
|
+
let o = `<path d="M${x} ${GROUND}C${x + 30 * m} 200 ${x - 22 * m} 150 ${x + 12 * m} 110S${x + 40 * m} 50 ${x + 6 * m} 28" fill="none" stroke="#4f7a74" stroke-width="3.2" stroke-linecap="round"/>`;
|
|
1148
|
+
for (const [lx, ly, a] of [[x + 14 * m, 200, 40], [x - 2 * m, 160, -30], [x + 20 * m, 120, 50], [x + 16 * m, 70, -40]]) {
|
|
1149
|
+
o += `<ellipse cx="${n(lx)}" cy="${ly}" rx="11" ry="4.5" fill="#8a9a6b" stroke="#4a3324" stroke-width="1" transform="rotate(${a * m} ${n(lx)} ${ly})"/>`;
|
|
1150
|
+
}
|
|
1151
|
+
for (const [fx, fy] of [[x + 6 * m, 28], [x + 26 * m, 96]]) {
|
|
1152
|
+
let pet = '';
|
|
1153
|
+
for (let i = 0; i < 5; i++) {
|
|
1154
|
+
const a = (i / 5) * Math.PI * 2 + rnd() * 0.3;
|
|
1155
|
+
pet += `<circle cx="${n(fx + Math.cos(a) * 6)}" cy="${n(fy + Math.sin(a) * 6)}" r="5" fill="#c98a7a" stroke="#4a3324" stroke-width=".8"/>`;
|
|
1156
|
+
}
|
|
1157
|
+
o += `${pet}<circle cx="${n(fx)}" cy="${fy}" r="3.2" fill="#c9a24a"/>`;
|
|
1158
|
+
}
|
|
1159
|
+
return o;
|
|
1160
|
+
};
|
|
1161
|
+
s += vine(20) + vine(280);
|
|
1162
|
+
// marco doble de arco y friso de mosaico
|
|
1163
|
+
s += `<path d="${arch}" fill="none" stroke="#4a3324" stroke-width="3"/><path d="M36 ${GROUND}V121A114 114 0 0 1 264 121V${GROUND}" fill="none" stroke="#c9a24a" stroke-width="1.5"/>`;
|
|
1164
|
+
s += `<rect y="${GROUND}" width="${W}" height="${W - GROUND}" fill="#8a9a6b"/><rect y="${GROUND}" width="${W}" height="4" fill="#4a3324"/>`;
|
|
1165
|
+
let tiles = '';
|
|
1166
|
+
for (let x = 6; x < W; x += 24) tiles += `<rect x="${x}" y="${GROUND + 14}" width="16" height="16" rx="3" fill="none" stroke="#c9a24a" stroke-width="1.6"/><circle cx="${x + 8}" cy="${GROUND + 22}" r="3" fill="#c98a7a"/>`;
|
|
1167
|
+
s += tiles + `<rect y="${GROUND + 40}" width="${W}" height="2" fill="#c9a24a"/>`;
|
|
1168
|
+
s += `<rect x="5" y="5" width="${W - 10}" height="${W - 10}" rx="10" fill="none" stroke="#4a3324" stroke-width="3"/><rect x="10" y="10" width="${W - 20}" height="${W - 20}" rx="7" fill="none" stroke="#c9a24a" stroke-width="1.2"/>`;
|
|
1169
|
+
s += `<rect width="${W}" height="${W}" filter="url(#${p}g)"/>`;
|
|
1170
|
+
return s;
|
|
1171
|
+
},
|
|
1172
|
+
|
|
1173
|
+
cubism(p, rnd) {
|
|
1174
|
+
let s = `<defs><linearGradient id="${p}a" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fff" stop-opacity=".3"/><stop offset="1" stop-color="#000" stop-opacity=".35"/></linearGradient><linearGradient id="${p}b" x1="1" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#000" stop-opacity=".4"/><stop offset="1" stop-color="#fff" stop-opacity=".2"/></linearGradient><linearGradient id="${p}c" x1="0" y1="1" x2="0" y2="0"><stop offset="0" stop-color="#000" stop-opacity=".3"/><stop offset=".6" stop-color="#000" stop-opacity="0"/></linearGradient>`
|
|
1175
|
+
+ `<filter id="${p}g" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency=".8" numOctaves="2" seed="${Math.floor(rnd() * 900)}"/><feColorMatrix values="0 0 0 0 .3 0 0 0 0 .24 0 0 0 0 .18 0 0 0 1 -.6"/></filter></defs>`;
|
|
1176
|
+
// planos facetados de Voronoi con su propia luz
|
|
1177
|
+
const sites = jitterGrid(rnd, 0, 0, W, W, 5, 5, 1.2);
|
|
1178
|
+
const cells = voronoi(sites, 0, 0, W, W);
|
|
1179
|
+
const pal = ['#c8a046', '#7a5232', '#7c7a46', '#4e6a86', '#b5583a', '#e6d6b0', '#8aa0a8', '#c8a046', '#e6d6b0'];
|
|
1180
|
+
let lines = '';
|
|
1181
|
+
cells.forEach((poly) => {
|
|
1182
|
+
const d = polyD(poly);
|
|
1183
|
+
s += `<path d="${d}" fill="${pal[Math.floor(rnd() * pal.length)]}"/><path d="${d}" fill="url(#${p}${'abc'[Math.floor(rnd() * 3)]})"/>`;
|
|
1184
|
+
lines += d;
|
|
1185
|
+
});
|
|
1186
|
+
s += `<path d="${lines}" fill="none" stroke="#2e2a26" stroke-width="2" stroke-linejoin="miter"/>`;
|
|
1187
|
+
// líneas de fuga que cruzan todo
|
|
1188
|
+
let cut = '';
|
|
1189
|
+
for (let i = 0; i < 4; i++) cut += `M${n(rnd() * W)} 0L${n(rnd() * W)} ${W}`;
|
|
1190
|
+
s += `<path d="${cut}" stroke="#2e2a26" stroke-width="1.4" opacity=".7"/>`;
|
|
1191
|
+
// guitarra fragmentada (mitad desplazada) y recorte de periódico
|
|
1192
|
+
const gx = 52;
|
|
1193
|
+
const gy = 120;
|
|
1194
|
+
const gtr = `M${gx} ${gy - 46}C${gx + 26} ${gy - 46} ${gx + 26} ${gy - 12} ${gx + 14} ${gy}C${gx + 34} ${gy + 14} ${gx + 30} ${gy + 56} ${gx} ${gy + 56}C${gx - 30} ${gy + 56} ${gx - 34} ${gy + 14} ${gx - 14} ${gy}C${gx - 26} ${gy - 12} ${gx - 26} ${gy - 46} ${gx} ${gy - 46}Z`;
|
|
1195
|
+
s += `<defs><clipPath id="${p}gl"><rect x="0" y="0" width="${gx}" height="${W}"/></clipPath><clipPath id="${p}gr"><rect x="${gx}" y="0" width="${W}" height="${W}"/></clipPath></defs>`;
|
|
1196
|
+
s += `<g clip-path="url(#${p}gl)"><path d="${gtr}" fill="#b07a3a" stroke="#2e2a26" stroke-width="2.4"/></g>`;
|
|
1197
|
+
s += `<g clip-path="url(#${p}gr)" transform="translate(6 -10)"><path d="${gtr}" fill="#7a5232" stroke="#2e2a26" stroke-width="2.4"/><path d="M${gx + 4} ${gy - 80}V${gy + 40}M${gx + 8} ${gy - 80}V${gy + 40}" stroke="#e6d6b0" stroke-width="1"/></g>`;
|
|
1198
|
+
s += `<circle cx="${gx}" cy="${gy + 16}" r="10" fill="#2e2a26"/><rect x="${gx - 5}" y="${gy - 110}" width="10" height="66" fill="#3a2e22"/>`;
|
|
1199
|
+
const rot = -12 + rnd() * 24;
|
|
1200
|
+
s += `<g transform="rotate(${n(rot)} 238 52)"><rect x="196" y="30" width="86" height="44" fill="#e9e1cc" stroke="#2e2a26" stroke-width="1.2"/><text x="239" y="54" text-anchor="middle" font-family="'Bodoni 72',Didot,'Times New Roman',serif" font-weight="700" font-size="20" fill="#2e2a26">JOUR</text><path d="M202 62H276M202 67H264" stroke="#2e2a26" stroke-width="1.6" opacity=".6"/></g>`;
|
|
1201
|
+
// mesa en escorzo
|
|
1202
|
+
s += `<path d="M-10 ${GROUND - 6}L310 ${GROUND + 6}L310 ${W}L-10 ${W}Z" fill="#7a5232"/><path d="M-10 ${GROUND - 6}L310 ${GROUND + 6}L310 ${W}L-10 ${W}Z" fill="url(#${p}c)"/>`;
|
|
1203
|
+
s += `<path d="M-10 ${GROUND + 14}L310 ${GROUND + 30}M-10 ${GROUND + 34}L310 ${GROUND + 54}M140 ${GROUND}L120 ${W}" stroke="#2e2a26" stroke-width="1.5" fill="none"/><path d="M-10 ${GROUND - 6}L310 ${GROUND + 6}" stroke="#2e2a26" stroke-width="3"/>`;
|
|
1204
|
+
s += `<rect width="${W}" height="${W}" filter="url(#${p}g)"/>`;
|
|
1205
|
+
return s;
|
|
1206
|
+
},
|
|
1207
|
+
|
|
1208
|
+
crayon(p, rnd) {
|
|
1209
|
+
const sd = Math.floor(rnd() * 900);
|
|
1210
|
+
let s = `<defs><filter id="${p}w" x="-5%" y="-5%" width="110%" height="110%"><feTurbulence type="fractalNoise" baseFrequency=".05" numOctaves="2" seed="${sd}" result="n"/><feDisplacementMap in="SourceGraphic" in2="n" scale="4" xChannelSelector="R" yChannelSelector="G" result="d"/><feTurbulence type="fractalNoise" baseFrequency=".9 .25" numOctaves="2" seed="${sd + 1}" result="wx"/><feColorMatrix in="wx" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -3 0 0 0 2.3" result="wa"/><feComposite in="d" in2="wa" operator="in"/></filter></defs>`;
|
|
1211
|
+
s += `<rect width="${W}" height="${W}" fill="#fffdf6"/>`;
|
|
1212
|
+
const zig = (x0, x1, y0, y1, step) => {
|
|
1213
|
+
let d = '';
|
|
1214
|
+
let up = true;
|
|
1215
|
+
for (let x = x0; x <= x1; x += step) {
|
|
1216
|
+
d += `${d ? 'L' : 'M'}${n(x + rnd() * 3)} ${n(up ? y0 + rnd() * 6 : y1 - rnd() * 6)}`;
|
|
1217
|
+
up = !up;
|
|
1218
|
+
}
|
|
1219
|
+
return d;
|
|
1220
|
+
};
|
|
1221
|
+
let g = '';
|
|
1222
|
+
// cielo: franja de zigzag azul arriba
|
|
1223
|
+
g += `<path d="${zig(-4, 304, -4, 44, 7)}" stroke="#5fa8e8" stroke-width="5"/>`;
|
|
1224
|
+
// sol con cara en la esquina
|
|
1225
|
+
g += '<circle cx="40" cy="44" r="22" fill="#f6d32b"/>';
|
|
1226
|
+
let ray = '';
|
|
1227
|
+
for (let i = 0; i < 10; i++) {
|
|
1228
|
+
const a = (i / 10) * Math.PI * 2 + 0.2;
|
|
1229
|
+
ray += `M${n(40 + Math.cos(a) * 28)} ${n(44 + Math.sin(a) * 28)}L${n(40 + Math.cos(a) * (40 + rnd() * 6))} ${n(44 + Math.sin(a) * (40 + rnd() * 6))}`;
|
|
1230
|
+
}
|
|
1231
|
+
g += `<path d="${ray}" stroke="#f58a1f" stroke-width="4"/><circle cx="33" cy="40" r="2.6" fill="#222"/><circle cx="47" cy="40" r="2.6" fill="#222"/><path d="M31 50Q40 58 49 50" stroke="#222" stroke-width="2.6"/>`;
|
|
1232
|
+
// nubes de borde ondulado, pájaros en M
|
|
1233
|
+
for (let k = 0; k < 2; k++) {
|
|
1234
|
+
const x = 120 + k * 90 + rnd() * 20;
|
|
1235
|
+
const y = 70 + rnd() * 20;
|
|
1236
|
+
g += `<path d="M${n(x)} ${n(y)}q6 -14 16 -6q8 -12 18 -2q12 -4 12 8q-2 8 -12 6h-30q-10 -2 -4 -6Z" fill="#fff" stroke="#2f6fd6" stroke-width="3"/>`;
|
|
1237
|
+
}
|
|
1238
|
+
for (let i = 0; i < 3; i++) {
|
|
1239
|
+
const x = 90 + rnd() * 140;
|
|
1240
|
+
const y = 108 + rnd() * 30;
|
|
1241
|
+
g += `<path d="M${n(x)} ${n(y)}q5 -7 9 0q4 -7 9 0" stroke="#222" stroke-width="2.4"/>`;
|
|
1242
|
+
}
|
|
1243
|
+
// casita (pared roja rellena a zigzag, techo marrón, puerta y ventana)
|
|
1244
|
+
const hx = 222 + rnd() * 18;
|
|
1245
|
+
g += `<rect x="${n(hx)}" y="172" width="56" height="${GROUND - 172}" fill="#ffd7cf"/><path d="${zig(hx, hx + 56, 174, GROUND - 2, 5)}" stroke="#e8352b" stroke-width="3" opacity=".75"/><rect x="${n(hx)}" y="172" width="56" height="${GROUND - 172}" stroke="#c22a20" stroke-width="3.5"/>`;
|
|
1246
|
+
g += `<path d="M${n(hx - 8)} 174L${n(hx + 28)} 138L${n(hx + 64)} 174Z" fill="#8b5a2b" stroke="#5a3a1a" stroke-width="3"/><rect x="${n(hx + 8)}" y="208" width="14" height="${GROUND - 208}" fill="#2f6fd6" stroke="#222" stroke-width="2.4"/><rect x="${n(hx + 32)}" y="186" width="16" height="16" fill="#f6d32b" stroke="#222" stroke-width="2.4"/><path d="M${n(hx + 40)} 186V202M${n(hx + 32)} 194H${n(hx + 48)}" stroke="#222" stroke-width="2"/>`;
|
|
1247
|
+
// pasto en zigzag verde y flores
|
|
1248
|
+
g += `<path d="${zig(-4, 304, GROUND - 10, GROUND + 4, 5)}" stroke="#4caf3c" stroke-width="3"/>`;
|
|
1249
|
+
g += `<path d="${zig(-4, 304, GROUND, W, 9)}" stroke="#7cc95a" stroke-width="5" opacity=".7"/>`;
|
|
1250
|
+
const fc = ['#e8352b', '#8a4fc4', '#f27bb5', '#f58a1f'];
|
|
1251
|
+
for (let i = 0; i < 4; i++) {
|
|
1252
|
+
const x = 16 + i * 50 + rnd() * 20;
|
|
1253
|
+
const y = GROUND + 14 + rnd() * 30;
|
|
1254
|
+
g += `<path d="M${n(x)} ${n(y)}V${n(y + 22)}" stroke="#2e8a2e" stroke-width="3"/><circle cx="${n(x)}" cy="${n(y)}" r="7" fill="${fc[i]}"/><circle cx="${n(x)}" cy="${n(y)}" r="2.6" fill="#f6d32b"/>`;
|
|
1255
|
+
}
|
|
1256
|
+
s += `<g fill="none" stroke-linecap="round" stroke-linejoin="round" filter="url(#${p}w)">${g}</g>`;
|
|
1257
|
+
return s;
|
|
1258
|
+
},
|
|
1259
|
+
|
|
1260
|
+
blueprint(p, rnd) {
|
|
1261
|
+
const line = '#eaf2ff';
|
|
1262
|
+
const mono = 'font-family="\'Courier New\',ui-monospace,monospace"';
|
|
1263
|
+
let s = `<defs><pattern id="${p}m" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M10 0H0V10" fill="none" stroke="${line}" stroke-width=".4" opacity=".22"/></pattern>`
|
|
1264
|
+
+ `<pattern id="${p}M" width="50" height="50" patternUnits="userSpaceOnUse"><path d="M50 0H0V50" fill="none" stroke="${line}" stroke-width=".8" opacity=".3"/></pattern>`
|
|
1265
|
+
+ `<pattern id="${p}h" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)"><path d="M0 0V6" stroke="${line}" stroke-width="1"/></pattern>`
|
|
1266
|
+
+ `<marker id="${p}ar" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 1L10 5L0 9Z" fill="${line}"/></marker>`
|
|
1267
|
+
+ `<filter id="${p}g" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency=".012" numOctaves="2" seed="${Math.floor(rnd() * 900)}"/><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 .5 0 0 0 -.18"/></filter></defs>`;
|
|
1268
|
+
s += `<rect width="${W}" height="${W}" fill="#1d4e89"/><rect width="${W}" height="${W}" filter="url(#${p}g)"/><rect width="${W}" height="${W}" fill="url(#${p}m)"/><rect width="${W}" height="${W}" fill="url(#${p}M)"/>`;
|
|
1269
|
+
s += `<rect x="8" y="8" width="${W - 16}" height="${W - 16}" fill="none" stroke="${line}" stroke-width="1.6"/><rect x="12" y="12" width="${W - 24}" height="${W - 24}" fill="none" stroke="${line}" stroke-width=".6"/>`;
|
|
1270
|
+
// cotas: ancho arriba y alto a la izquierda
|
|
1271
|
+
const w = 150 + Math.round(rnd() * 6) * 10;
|
|
1272
|
+
s += `<g stroke="${line}" stroke-width="1" fill="none"><path d="M${150 - 90} 46V60M${150 + 90} 46V60M30 60H44M30 ${GROUND}H44"/><path d="M${150 - 88} 52H${150 + 88}" marker-start="url(#${p}ar)" marker-end="url(#${p}ar)"/><path d="M36 62V${GROUND - 2}" marker-start="url(#${p}ar)" marker-end="url(#${p}ar)"/></g>`;
|
|
1273
|
+
s += `<text x="150" y="47" text-anchor="middle" ${mono} font-size="10" fill="${line}">${w} mm</text><text x="31" y="${(60 + GROUND) / 2}" text-anchor="middle" transform="rotate(-90 31 ${(60 + GROUND) / 2})" ${mono} font-size="10" fill="${line}">${w + 20 + Math.round(rnd() * 4) * 10} mm</text>`;
|
|
1274
|
+
// trazos de construcción: arcos de compás y una rosa de los vientos
|
|
1275
|
+
s += `<g stroke="${line}" fill="none" stroke-width=".8" opacity=".6" stroke-dasharray="5 3"><circle cx="150" cy="150" r="118"/><path d="M14 150H286M150 20V${GROUND}"/></g>`;
|
|
1276
|
+
s += `<g stroke="${line}" fill="none" stroke-width="1"><circle cx="258" cy="44" r="16"/><circle cx="258" cy="44" r="3"/><path d="M258 22V66M236 44H280"/></g><path d="M258 26L262 44H254Z" fill="${line}"/><text x="258" y="22" text-anchor="middle" ${mono} font-size="8" fill="${line}" dy="-2">N</text>`;
|
|
1277
|
+
// engranaje de detalle
|
|
1278
|
+
let gear = '';
|
|
1279
|
+
for (let i = 0; i < 10; i++) {
|
|
1280
|
+
const a = (i / 10) * Math.PI * 2;
|
|
1281
|
+
gear += `M${n(262 + Math.cos(a) * 14)} ${n(150 + Math.sin(a) * 14)}L${n(262 + Math.cos(a) * 19)} ${n(150 + Math.sin(a) * 19)}`;
|
|
1282
|
+
}
|
|
1283
|
+
s += `<g stroke="${line}" fill="none" stroke-width="1.2"><circle cx="262" cy="150" r="14"/><circle cx="262" cy="150" r="5"/><path d="${gear}" stroke-width="3"/><path d="M248 136L226 116H210" stroke-width=".8"/></g><text x="208" y="112" ${mono} font-size="8" fill="${line}">DET. A</text>`;
|
|
1284
|
+
// suelo en sección (rayado) y cajetín
|
|
1285
|
+
s += `<rect y="${GROUND}" width="${W}" height="12" fill="url(#${p}h)" opacity=".7"/><path d="M12 ${GROUND}H${W - 12}" stroke="${line}" stroke-width="1.8"/>`;
|
|
1286
|
+
s += `<g stroke="${line}" fill="none" stroke-width="1"><rect x="176" y="256" width="112" height="32"/><path d="M176 272H288M232 256V288"/></g>`;
|
|
1287
|
+
s += `<g ${mono} font-size="8" fill="${line}"><text x="180" y="267">7OTS</text><text x="236" y="267">OT-${String(Math.floor(rnd() * 900) + 100)}</text><text x="180" y="283">ESC 1:1</text><text x="236" y="283">HOJA 7/7</text></g>`;
|
|
1288
|
+
return s;
|
|
1289
|
+
},
|
|
1290
|
+
|
|
1291
|
+
pointillism(p, rnd) {
|
|
1292
|
+
// cada color es una trama de toques: base clara + puntos del color, más claros, más oscuros y vecinos
|
|
1293
|
+
let defs = '';
|
|
1294
|
+
let k = 0;
|
|
1295
|
+
const dots = (base) => {
|
|
1296
|
+
const id = `${p}p${k++}`;
|
|
1297
|
+
const [h, s0, l] = toHsl(base);
|
|
1298
|
+
const cols = [base, base, fromHsl(h, s0, Math.min(0.92, l + 0.18)), fromHsl(h, s0, Math.max(0.12, l - 0.16)), fromHsl((h + 30) % 360, Math.min(1, s0 + 0.1), l), fromHsl((h + 330) % 360, s0, l), fromHsl((h + 180) % 360, 0.5, 0.7), '#fffaf0'];
|
|
1299
|
+
let c = '';
|
|
1300
|
+
// rejilla con jitter (sin huecos ni grumos) en una tesela grande: no se nota la repetición
|
|
1301
|
+
jitterGrid(rnd, 0, 0, 20, 20, 7, 7, 0.9).forEach(([x, y], i) => {
|
|
1302
|
+
const r = n(1.05 + rnd() * 0.6);
|
|
1303
|
+
const f = cols[(i * 5 + Math.floor(rnd() * 3)) % cols.length];
|
|
1304
|
+
// copias al otro lado de la tesela para los que asoman por el borde
|
|
1305
|
+
const wx = x < 2 ? 20 : x > 18 ? -20 : 0;
|
|
1306
|
+
const wy = y < 2 ? 20 : y > 18 ? -20 : 0;
|
|
1307
|
+
const at = [[0, 0], ...(wx ? [[wx, 0]] : []), ...(wy ? [[0, wy]] : []), ...(wx && wy ? [[wx, wy]] : [])];
|
|
1308
|
+
for (const [dx, dy] of at) c += `<circle cx="${n(x + dx)}" cy="${n(y + dy)}" r="${r}" fill="${f}"/>`;
|
|
1309
|
+
});
|
|
1310
|
+
defs += `<pattern id="${id}" width="20" height="20" patternUnits="userSpaceOnUse"><rect width="20" height="20" fill="${mix(base, '#fffaf0', 0.5)}"/>${c}</pattern>`;
|
|
1311
|
+
return `url(#${id})`;
|
|
1312
|
+
};
|
|
1313
|
+
let s = '';
|
|
1314
|
+
// cielo, río, orilla lejana y árboles, pasto al sol y a la sombra (La Grande Jatte)
|
|
1315
|
+
s += `<rect width="${W}" height="140" fill="${dots('#9cc4e4')}"/>`;
|
|
1316
|
+
s += `<rect y="120" width="${W}" height="40" fill="${dots('#3f78b8')}"/>`;
|
|
1317
|
+
s += `<path d="M0 128Q60 112 120 124T240 118T300 122V134H0Z" fill="${dots('#5d8a4a')}"/>`;
|
|
1318
|
+
s += `<rect y="156" width="${W}" height="${W - 156}" fill="${dots('#7fb24a')}"/>`;
|
|
1319
|
+
s += `<path d="M0 ${W}V200Q90 ${GROUND - 20} 180 ${GROUND + 10}T${W} ${GROUND}V${W}Z" fill="${dots('#3c6e3a')}"/>`;
|
|
1320
|
+
const tx = rnd() < 0.5 ? 34 : 262;
|
|
1321
|
+
s += `<rect x="${tx - 6}" y="40" width="12" height="${GROUND - 34}" fill="${dots('#5a4030')}"/>`;
|
|
1322
|
+
s += `<ellipse cx="${tx}" cy="54" rx="58" ry="52" fill="${dots('#2f5a34')}"/>`;
|
|
1323
|
+
// velero y sombrilla en silueta
|
|
1324
|
+
const bx = 120 + rnd() * 60;
|
|
1325
|
+
s += `<path d="M${n(bx)} 132L${n(bx)} 92L${n(bx + 22)} 128Z" fill="${dots('#f3ead8')}"/><path d="M${n(bx - 10)} 132H${n(bx + 26)}L${n(bx + 20)} 138H${n(bx - 4)}Z" fill="${dots('#8a3a2a')}"/>`;
|
|
1326
|
+
const ux = tx < 150 ? 238 : 62;
|
|
1327
|
+
s += `<path d="M${ux - 26} 186Q${ux} 160 ${ux + 26} 186Z" fill="${dots('#c84a4a')}"/><path d="M${ux} 184V${GROUND - 6}" stroke="#3a2a20" stroke-width="2"/>`;
|
|
1328
|
+
// marco pintado de puntos (como hacía Seurat)
|
|
1329
|
+
s += `<path d="M0 0H${W}V${W}H0ZM10 10V${W - 10}H${W - 10}V10Z" fill-rule="evenodd" fill="${dots('#2a3f7a')}"/>`;
|
|
1330
|
+
return `<defs>${defs}</defs>${s}`;
|
|
1331
|
+
},
|
|
1332
|
+
|
|
1333
|
+
woodcut(p, rnd) {
|
|
1334
|
+
const ink = '#15120f';
|
|
1335
|
+
const pap = '#ecdfc4';
|
|
1336
|
+
let s = `<defs><filter id="${p}g" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency=".55" numOctaves="2" seed="${Math.floor(rnd() * 900)}"/><feColorMatrix values="0 0 0 0 .93 0 0 0 0 .87 0 0 0 0 .77 0 0 0 2.2 -1.25"/></filter>`
|
|
1337
|
+
+ `<filter id="${p}r" x="-5%" y="-5%" width="110%" height="110%"><feTurbulence type="fractalNoise" baseFrequency=".06" numOctaves="2" seed="${Math.floor(rnd() * 900)}"/><feDisplacementMap in="SourceGraphic" scale="3" xChannelSelector="R" yChannelSelector="G"/></filter></defs>`;
|
|
1338
|
+
s += `<rect width="${W}" height="${W}" fill="${pap}"/>`;
|
|
1339
|
+
let g = `<rect x="10" y="10" width="${W - 20}" height="${W - 20}" fill="${ink}"/>`;
|
|
1340
|
+
// cielo negro con tallas onduladas
|
|
1341
|
+
let cuts = '';
|
|
1342
|
+
for (let y = 22; y < 180; y += 9) {
|
|
1343
|
+
let x = 12 + rnd() * 20;
|
|
1344
|
+
while (x < W - 30) {
|
|
1345
|
+
const len = 20 + rnd() * 50;
|
|
1346
|
+
const a = 1.5 + rnd() * 2;
|
|
1347
|
+
cuts += `M${n(x)} ${n(y)}q${n(len / 4)} ${n(-a)} ${n(len / 2)} 0t${n(len / 2)} 0`;
|
|
1348
|
+
x += len + 8 + rnd() * 14;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
g += `<path d="${cuts}" fill="none" stroke="${pap}" stroke-width="1.6" stroke-linecap="round"/>`;
|
|
1352
|
+
// sol rojo con anillos tallados y rayos en cuña
|
|
1353
|
+
const sx = 200 + rnd() * 50;
|
|
1354
|
+
const sy = 64;
|
|
1355
|
+
let rays = '';
|
|
1356
|
+
for (let i = 0; i < 18; i++) {
|
|
1357
|
+
const a = (i / 18) * Math.PI * 2;
|
|
1358
|
+
const a1 = a + 0.06;
|
|
1359
|
+
rays += `M${n(sx + Math.cos(a) * 34)} ${n(sy + Math.sin(a) * 34)}L${n(sx + Math.cos(a - 0.06) * 58)} ${n(sy + Math.sin(a - 0.06) * 58)}L${n(sx + Math.cos(a1) * 58)} ${n(sy + Math.sin(a1) * 58)}Z`;
|
|
1360
|
+
}
|
|
1361
|
+
g += `<circle cx="${n(sx)}" cy="${sy}" r="58" fill="${ink}"/><path d="${rays}" fill="${pap}"/><circle cx="${n(sx)}" cy="${sy}" r="28" fill="#b8352a"/>`;
|
|
1362
|
+
g += `<circle cx="${n(sx)}" cy="${sy}" r="20" fill="none" stroke="${pap}" stroke-width="1.6"/><circle cx="${n(sx)}" cy="${sy}" r="12" fill="none" stroke="${pap}" stroke-width="1.4"/>`;
|
|
1363
|
+
// montañas claras con rayado negro siguiendo la ladera
|
|
1364
|
+
const ridge = (base, amp) => {
|
|
1365
|
+
const pts = [];
|
|
1366
|
+
for (let x = 0; x <= W; x += 30) pts.push([x, base - amp * (0.3 + rnd() * 0.7)]);
|
|
1367
|
+
return pts;
|
|
1368
|
+
};
|
|
1369
|
+
const pts = ridge(196, 70);
|
|
1370
|
+
const dOf = (dy) => `M0 ${n(pts[0][1] + dy)}${pts.map(([x, y]) => `L${x} ${n(y + dy)}`).join('')}`;
|
|
1371
|
+
g += `<path d="${dOf(0)}L${W} ${GROUND}L0 ${GROUND}Z" fill="${pap}"/>`;
|
|
1372
|
+
let hatch = '';
|
|
1373
|
+
for (let dy = 8; dy < 90; dy += 7) hatch += dOf(dy);
|
|
1374
|
+
g += `<path d="${hatch}" fill="none" stroke="${ink}" stroke-width="${n(2.2)}" stroke-linejoin="round"/>`;
|
|
1375
|
+
// pino tallado a un lado
|
|
1376
|
+
const tx = sx > 225 ? 46 : 254;
|
|
1377
|
+
let pine = '';
|
|
1378
|
+
for (let i = 0; i < 5; i++) {
|
|
1379
|
+
const y = 120 + i * 22;
|
|
1380
|
+
const w = 12 + i * 7;
|
|
1381
|
+
pine += `M${tx} ${y - 20}L${tx + w} ${y + 8}L${tx - w} ${y + 8}Z`;
|
|
1382
|
+
}
|
|
1383
|
+
g += `<path d="${pine}" fill="${ink}" stroke="${pap}" stroke-width="1.2"/><rect x="${tx - 4}" y="${GROUND - 14}" width="8" height="14" fill="${ink}"/>`;
|
|
1384
|
+
// suelo negro con golpes de gubia
|
|
1385
|
+
g += `<rect x="10" y="${GROUND}" width="${W - 20}" height="${W - GROUND - 10}" fill="${ink}"/>`;
|
|
1386
|
+
let gouge = '';
|
|
1387
|
+
for (let i = 0; i < 34; i++) {
|
|
1388
|
+
const x = 16 + rnd() * (W - 40);
|
|
1389
|
+
const y = GROUND + 8 + rnd() * 40;
|
|
1390
|
+
gouge += `M${n(x)} ${n(y)}l${n(8 + rnd() * 12)} ${n(-1 - rnd() * 2)}`;
|
|
1391
|
+
}
|
|
1392
|
+
g += `<path d="M10 ${GROUND + 2}H${W - 10}" stroke="${pap}" stroke-width="2"/><path d="${gouge}" stroke="${pap}" stroke-width="2" stroke-linecap="round"/>`;
|
|
1393
|
+
s += `<g filter="url(#${p}r)">${g}</g>`;
|
|
1394
|
+
// tinta desigual: motas de papel que asoman
|
|
1395
|
+
s += `<rect width="${W}" height="${W}" filter="url(#${p}g)"/>`;
|
|
1396
|
+
return s;
|
|
1397
|
+
},
|
|
1398
|
+
};
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* Contenido del fondo de un estilo en el marco 0 0 300 300 ('' sin estilo).
|
|
1402
|
+
* @param {string} style
|
|
1403
|
+
* @param {number} seed styleSeed(spec)
|
|
1404
|
+
* @param {string} [prefix] prefijo de ids (único si el fondo va dentro de otro SVG)
|
|
1405
|
+
*/
|
|
1406
|
+
export function styleBackdrop(style, seed, prefix = 'bd') {
|
|
1407
|
+
const f = BACKDROPS[style];
|
|
1408
|
+
return f ? f(`${prefix}-`, seeded((seed ^ 0x9e3779b9) >>> 0)) : '';
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/** Fondo como documento SVG independiente (para background-image o un <img>). */
|
|
1412
|
+
export function backdropSVG(style, seed) {
|
|
1413
|
+
const inner = styleBackdrop(style, seed);
|
|
1414
|
+
return inner ? `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${W}" preserveAspectRatio="xMidYMax slice">${inner}</svg>` : '';
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/** Fondo como url("data:…") para CSS (se rasteriza una vez: no cuesta nada al animar). */
|
|
1418
|
+
export function backdropCSS(style, seed) {
|
|
1419
|
+
const svg = backdropSVG(style, seed);
|
|
1420
|
+
return svg ? `url("data:image/svg+xml,${encodeURIComponent(svg)}")` : '';
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
/** Fondo anidado que cubre un viewBox 'x y w h' (para SVG estáticos). */
|
|
1424
|
+
export function backdropIn(style, seed, viewBox, prefix) {
|
|
1425
|
+
const inner = styleBackdrop(style, seed, prefix);
|
|
1426
|
+
if (!inner) return '';
|
|
1427
|
+
const [x, y, w, h] = String(viewBox).split(/[\s,]+/).map(Number);
|
|
1428
|
+
return `<svg x="${x}" y="${y}" width="${w}" height="${h}" viewBox="0 0 ${W} ${W}" preserveAspectRatio="xMidYMax slice" overflow="hidden">${inner}</svg>`;
|
|
1429
|
+
}
|