|
| 1 | +// the image in the README: a weather card of frosted glass over an SVG landscape |
1 | 2 | const {Image} = require('..'); |
2 | | -const fs = require('fs').promises; |
| 3 | +const fs = require('fs'); |
3 | 4 | const samePixels = require('./util/pixels'); |
4 | 5 |
|
5 | | -(async () => { |
6 | | - const [backgroundSVG, avatarBinary, badges, font] = await Promise.all([ |
7 | | - fs.readFile('./tests/svgs/background.svg').then(b => b.toString()), |
8 | | - fs.readFile('./tests/targets/external.png'), |
9 | | - Promise.all( |
10 | | - [ |
11 | | - 'crown', 'potato', 'mask', 'microbe', 'petri_dish', 'thermometer', 'cigarette' |
12 | | - ].map( |
13 | | - x => fs.readFile(`./tests/svgs/${x}.svg`) |
14 | | - .then(b => b.toString())) |
15 | | - ), |
16 | | - fs.readFile('./tests/fonts/carbon phyber.ttf') |
17 | | - ]); |
18 | | - |
19 | | - const image = new Image(1000, 700); |
20 | | - |
21 | | - const backgroundPattern = await Image.renderSVG(backgroundSVG); |
22 | | - for (let xOffset = 0; xOffset < image.width; xOffset += backgroundPattern.width) { |
23 | | - for (let yOffset = 0; yOffset < image.height; yOffset += backgroundPattern.height) { |
24 | | - image.composite(backgroundPattern, {x: xOffset, y: yOffset}); |
25 | | - } |
26 | | - } |
27 | | - |
28 | | - const avatarBG = new Image(200 + 16, 200 + 16); |
29 | | - const avatarGradient = Image.gradient({0: 0x00ffffff, 1: 0x0080ffff}); |
30 | | - avatarBG.fill((x, y) => avatarGradient((x + y) / (avatarBG.width + avatarBG.height))); |
31 | | - image.composite(avatarBG.cropCircle(), {x: 0.05 * image.width, y: 0.05 * image.height}); |
32 | | - |
33 | | - const avatar = await Image.decode(avatarBinary); |
34 | | - image.composite(avatar.resize(200, 200).cropCircle(), {x: 0.05 * image.width + 8, y: 0.05 * image.height + 8}); |
35 | | - |
36 | | - const username = await Image.renderText(font, 64, 'matmen', {color: 0xffffffff}); |
37 | | - image.composite(username, {x: 0.3 * image.width, y: 0.05 * image.height}); |
38 | | - |
39 | | - image.drawBox(0.3 * image.width, 0.05 * image.height + username.height, username.width, 3, 0x808080ff); |
40 | | - |
41 | | - const discriminator = await Image.renderText(font, 48, '#9984', {color: 0xd0d0d0ff}); |
42 | | - image.composite(discriminator, {x: 0.3 * image.width, y: 0.05 * image.height + username.height}); |
43 | | - |
44 | | - // Global XP |
45 | | - { |
46 | | - const xpBarBackground = new Image(0.9 * image.width, 0.05 * image.height); |
47 | | - xpBarBackground.fill(0xa0a0a0ff); |
48 | | - image.composite(xpBarBackground.roundCorners(16), {x: 0.05 * image.width, y: 0.5 * image.height}); |
49 | | - |
50 | | - const xpBar = new Image(69 / 420 * xpBarBackground.width, 0.05 * image.height); |
51 | | - const xpGradient = Image.gradient({0: 0x0080ffff, .8: 0x00ffffff, 1: 0x00ff80ff}); |
52 | | - xpBar.fill(x => xpGradient(x / xpBarBackground.width)); |
53 | | - image.composite(xpBar.roundCorners(16), {x: 0.05 * image.width, y: 0.5 * image.height}); |
54 | | - |
55 | | - const xpText = await Image.renderText(font, 32, '69/420', {color: 0xff}); |
56 | | - image.composite(xpText, {x: 0.05 * image.width + xpBarBackground.width / 2 - xpText.width / 2, y: 0.5 * image.height + xpBarBackground.height / 2 - xpText.height / 2}); |
| 6 | +const font = name => fs.readFileSync('./tests/fonts/' + name); |
57 | 7 |
|
58 | | - const levelText = await Image.renderText(font, 32, 'Level 3', {color: 0xffffffff}); |
59 | | - image.composite(levelText, {x: 0.05 * image.width + xpBarBackground.width - levelText.width, y: 0.5 * image.height - levelText.height}); |
60 | | - |
61 | | - const xpKindText = await Image.renderText(font, 32, 'Global XP', {color: 0xffffffff}); |
62 | | - image.composite(xpKindText, {x: 0.05 * image.width, y: 0.5 * image.height - levelText.height}); |
63 | | - } |
64 | | - |
65 | | - // Server XP |
66 | | - { |
67 | | - const xpBarBackground = new Image(0.9 * image.width, 0.05 * image.height); |
68 | | - xpBarBackground.fill(0xa0a0a0ff); |
69 | | - image.composite(xpBarBackground.roundCorners(16), {x: 0.05 * image.width, y: 0.65 * image.height}); |
70 | | - |
71 | | - const xpBar = new Image(75 / 100 * xpBarBackground.width, 0.05 * image.height); |
72 | | - const xpGradient = Image.gradient({0: 0x0080ffff, .8: 0x00ffffff, 1: 0x00ff80ff}); |
73 | | - xpBar.fill(x => xpGradient(x / xpBarBackground.width)); |
74 | | - image.composite(xpBar.roundCorners(16), {x: 0.05 * image.width, y: 0.65 * image.height}); |
75 | | - |
76 | | - const xpText = await Image.renderText(font, 32, '75/100', {color: 0xff}); |
77 | | - image.composite(xpText, {x: 0.05 * image.width + xpBarBackground.width / 2 - xpText.width / 2, y: 0.65 * image.height + xpBarBackground.height / 2 - xpText.height / 2}); |
78 | | - |
79 | | - const levelText = await Image.renderText(font, 32, 'Level 1', {color: 0xffffffff}); |
80 | | - image.composite(levelText, {x: 0.05 * image.width + xpBarBackground.width - levelText.width, y: 0.65 * image.height - levelText.height}); |
81 | | - |
82 | | - const xpKindText = await Image.renderText(font, 32, 'Server XP', {color: 0xffffffff}); |
83 | | - image.composite(xpKindText, {x: 0.05 * image.width, y: 0.65 * image.height - levelText.height}); |
84 | | - } |
85 | | - |
86 | | - const badgeBackground = new Image(...Array(2).fill(Math.sqrt(64 ** 2 * 2))); |
87 | | - badgeBackground.fill(0xa0a0a080); |
88 | | - badgeBackground.cropCircle(); |
89 | | - for (let i = 0; i < badges.length; i++) { |
90 | | - image.composite(badgeBackground, {x: 0.05 * image.width + 0.9 * image.width / badges.length * i, y: 0.8 * image.height}); |
91 | | - |
92 | | - const badge = await Image.renderSVG(badges[i], {size: 64, mode: Image.SVG_MODE_WIDTH}); |
93 | | - if (i >= 3) |
94 | | - badge.saturation(0); |
95 | | - |
96 | | - image.composite(badge, { |
97 | | - x: 0.05 * image.width + 0.9 * image.width / badges.length * i + (badgeBackground.width - badge.width) / 2, |
98 | | - y: 0.8 * image.height + (badgeBackground.height - badge.height) / 2 |
99 | | - }); |
| 8 | +(async () => { |
| 9 | + const W = 1000, H = 600; |
| 10 | + const bold = font('opensans bold.ttf'), regular = font('sourcesans3 regular.otf'); |
| 11 | + const scripts = [regular, font('noto naskh arabic subset.ttf'), font('noto sans devanagari subset.ttf'), font('noto colrv1 subset.ttf')]; |
| 12 | + |
| 13 | + // background: an SVG landscape at sunrise |
| 14 | + const image = await Image.renderSVG(`<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}"> |
| 15 | + <defs> |
| 16 | + <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"> |
| 17 | + <stop offset="0" stop-color="#2b3a8c"/><stop offset="0.55" stop-color="#e2729a"/><stop offset="1" stop-color="#ffc58a"/> |
| 18 | + </linearGradient> |
| 19 | + <radialGradient id="sun"><stop offset="0" stop-color="#fff6d0"/><stop offset="0.5" stop-color="#ffd27a"/><stop offset="1" stop-color="#ffd27a" stop-opacity="0"/></radialGradient> |
| 20 | + <linearGradient id="far" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#7a4d8c"/><stop offset="1" stop-color="#4a3366"/></linearGradient> |
| 21 | + <linearGradient id="near" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#2c2347"/><stop offset="1" stop-color="#16122a"/></linearGradient> |
| 22 | + </defs> |
| 23 | + <rect width="${W}" height="${H}" fill="url(#sky)"/> |
| 24 | + <circle cx="760" cy="330" r="190" fill="url(#sun)"/> |
| 25 | + <circle cx="760" cy="330" r="62" fill="#fff3c4"/> |
| 26 | + <path d="M0 420 L120 330 L210 380 L330 280 L470 390 L560 340 L700 410 L820 320 L1000 400 L1000 600 L0 600 Z" fill="url(#far)"/> |
| 27 | + <path d="M0 500 C150 430 260 470 380 440 S620 380 760 460 S920 470 1000 440 L1000 600 L0 600 Z" fill="url(#near)"/> |
| 28 | + </svg>`); |
| 29 | + |
| 30 | + // a frosted glass card: the background behind it, blurred and lightened, with a soft shadow |
| 31 | + const card = {x: 40, y: 40, width: 520, height: 520, radius: 28}; |
| 32 | + const shadow = new Image(card.width + 80, card.height + 80).drawBox(40, 48, card.width, card.height, 0x00000060).blur(16); |
| 33 | + image.composite(shadow, card.x - 40, card.y - 40); |
| 34 | + const glass = image.region(card.x, card.y, card.width, card.height).blur(24) |
| 35 | + .composite(new Image(card.width, card.height).fill(0xffffffff), 0, 0, {opacity: 0.18}) |
| 36 | + .roundCorners(card.radius); |
| 37 | + image.composite(glass, card.x, card.y); |
| 38 | + |
| 39 | + const text = async (fonts, size, string, color, x, y) => { |
| 40 | + const rendered = await Image.renderText(fonts, size, string, {color}); |
| 41 | + image.composite(rendered, x, y); |
| 42 | + return rendered; |
| 43 | + }; |
| 44 | + const left = card.x + 40; |
| 45 | + await text(bold, 44, 'Lisbon', 0xffffffff, left, card.y + 32); |
| 46 | + await text(regular, 26, 'Sunrise, clearing up later', 0xffffffd0, left, card.y + 92); |
| 47 | + const temperature = await text(bold, 132, '21°', 0xffffffff, left - 6, card.y + 128); |
| 48 | + |
| 49 | + // a sun behind a cloud |
| 50 | + const icon = await Image.renderSVG(`<svg xmlns="http://www.w3.org/2000/svg" width="160" height="120" viewBox="0 0 160 120"> |
| 51 | + <defs><linearGradient id="s" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#ffe27a"/><stop offset="1" stop-color="#ff9a3c"/></linearGradient> |
| 52 | + <linearGradient id="c" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#dfe6f5"/></linearGradient></defs> |
| 53 | + <circle cx="62" cy="48" r="34" fill="url(#s)"/> |
| 54 | + <path d="M44 108 a26 26 0 0 1 4 -52 a34 34 0 0 1 64 8 a22 22 0 0 1 6 44 Z" fill="url(#c)"/> |
| 55 | + </svg>`); |
| 56 | + image.composite(icon, card.x + card.width - 40 - icon.width, card.y + 150); |
| 57 | + |
| 58 | + // the week: bars from the lowest to the highest temperature, in a gradient from cool to warm |
| 59 | + const days = [['Mon', 14, 21], ['Tue', 13, 19], ['Wed', 15, 23], ['Thu', 16, 25], ['Fri', 14, 22], ['Sat', 12, 18], ['Sun', 13, 20]]; |
| 60 | + const gradient = Image.gradient({0: 0x5ac8faff, 0.5: 0xffd166ff, 1: 0xff7a59ff}); |
| 61 | + const chart = {top: card.y + 300, height: 100, min: 10, max: 26}; |
| 62 | + const step = (card.width - 80) / days.length; |
| 63 | + for (const [i, [day, low, high]] of days.entries()) { |
| 64 | + const cx = left + step * i + step / 2; |
| 65 | + const y0 = chart.top + chart.height * (1 - (high - chart.min) / (chart.max - chart.min)); |
| 66 | + const y1 = chart.top + chart.height * (1 - (low - chart.min) / (chart.max - chart.min)); |
| 67 | + const track = new Image(14, chart.height).fill(0xffffff30).roundCorners(7); |
| 68 | + image.composite(track, cx - 7, chart.top); |
| 69 | + const bar = new Image(14, y1 - y0).fill((x, y) => gradient((chart.max - (y0 + y - chart.top) / chart.height * (chart.max - chart.min) - chart.min) / (chart.max - chart.min))); |
| 70 | + image.composite(bar.roundCorners(7), cx - 7, y0); |
| 71 | + const label = await Image.renderText(regular, 20, day, {color: 0xffffffc0}); |
| 72 | + image.composite(label, cx - label.width / 2, chart.top + chart.height + 10); |
100 | 73 | } |
101 | 74 |
|
102 | | - image.roundCorners(32); |
| 75 | + // a divider, then greetings in a few scripts |
| 76 | + image.drawBox(left, card.y + 446, card.width - 80, 1, 0xffffff40); |
| 77 | + const greeting = await Image.renderText(scripts, 26, 'Bom dia · مرحبا · नमस्ते 😀', {color: 0xffffffe0}); |
| 78 | + image.composite(greeting, left, card.y + 446 + (card.height - 446 - greeting.height) / 2); |
103 | 79 |
|
| 80 | + image.roundCorners(24); |
104 | 81 | const encoded = await image.encode({metadata: {creationTime: 0, software: ''}}); |
105 | 82 |
|
106 | 83 | if (process.env.OVERWRITE_TEST) |
107 | | - await fs.writeFile('./tests/targets/readme.png', encoded); |
| 84 | + fs.writeFileSync('./tests/targets/readme.png', encoded); |
108 | 85 |
|
109 | | - if (!await samePixels(await fs.readFile('./tests/targets/readme.png'), encoded)) |
| 86 | + if (!await samePixels(fs.readFileSync('./tests/targets/readme.png'), encoded)) |
110 | 87 | process.exit(1); |
111 | 88 | })(); |
0 commit comments