Здесь делается вжух 🪄

Рядом с владельцем аптеки она так же увидела рыжеволосую молодую женщину, которая тут же подошла к ней выпалила пару фраз и счастливая испарилась. Так что Туни успела лишь только вымолвить — Взаимно, — ворчливым она Арсениуса, конечно, не считала, но из слов последнего поняла, что видимо у него довольно близкие отношения с помощницей, если у них позволительно такие шутки шутить. Вот у них в Гранингс вряд ли бы кто-то так пошутил при их начальнике. Маги одним словом.Petunia Evans

Скептический взгляд Арсениуса метнулся к Дамоклу. К какой это такой “подобной ситуации” он готовился? В планах самого Джиггера в принципе отсутствовал пункт, где он должен будет обратиться к вампирам за помощью, но комментарии волшебник оставил при себе. Возможно, у друга есть какой-то план по созданию сети должников на разные случаи жизни, Арсениус не удивился бы, если у целителя уже в книжечке “запросить поддержку” числились пикси, драконы и тролли.Arsenius Jigger

— Не знал, но не удивлюсь, если её всё равно едят. Ты наверняка можешь правильно её приготовить? — Рыба-фугу, вероятно, не водится в реках Великобритании, если уж Фебус, пусть и далёкий от интереса к названию зверья без необходимости, совсем не слышал про такую. Для него всё гораздо проще: поймал — значит можно сожрать. А если оно окажется ядовитым — с большой вероятностью организм оборотня переварит такую тварь без фатальных последствий.Phoebus Howling

Скептический взгляд Арсениуса метнулся к Дамоклу. К какой это такой “подобной ситуации” он готовился? В планах самого Джиггера в принципе отсутствовал пункт, где он должен будет обратиться к вампирам за помощью, но комментарии волшебник оставил при себе. Возможно, у друга есть какой-то план по созданию сети должников на разные случаи жизни, Арсениус не удивился бы, если у целителя уже в книжечке “запросить поддержку” числились пикси, драконы и тролли.Arsenius Jigger

«Шлёп. Бум. Шмяк» — раздавалось за дверью, и Лит обречённо вздохнул. По всему выходило, что спокойный сон ему не светит: за физзгигом стоило приглядеть, чтобы тот не поранился в попытках выбраться. Неугомонное создание. Стащив с кровати подушку и одеяло, он велел животным не волноваться и снова зашёл в комнату, где обосновался клыкастый зверь. На полу тут и там виднелись капли крови, но, к счастью, принадлежали они не физзгигу, а всего лишь уже сожранному кролику. Дверь захлопнулась, отрезая обоим путь к побегу.Hippolytus Scamander

На коварный вопрос “верила ли она в преображение” Симона стыдливо промолчала. Отвечать честно утвердительно не позволяло чувство такта и благодарности (все-таки ведьма выполнила свою часть уговора, не бросила апеллируя к собственной неуклюжести итальянки), а врать не хотелось. Почему-то думалось, что ложь Локуста распознает на раз. И не оценит, даже если то будет ложь из вежливости.Simona Fiorelli

Когда этот день начинался, то Джу и представить не могла, что они увидят нечто настолько ужасное. И дело даже не в пропаже людей, искусственно созданном пространстве или каких-то монстрах. Дело в людях, когда речь о чём-то страшном, то дело всегда в них.Judith Milligan

Такого вопиющего в своей наглости поведения Долиш стерпеть не мог. — Не вали с больной головы на здоровую! Сам небось вообще не на тот рецепт смотришь. Одиннадцатая страница. Один-над-ца-тая, — Джон тянет по слогам, глядя на одноклассника с пренебрежительной снисходительностью. — Понимаю, это целое двухзначное число и тебе может быть тяжело сосчитать до него, но это Хогвартс, тут тебе придется сложно, если не сделаешь усилие над своей умственной неполноценностью.John Dawlish

Северус входит и останавливается. Запах трав и лекарственных зелий сразу ударяет в нос, но все равно ему кажется, что здесь все пахнет иначе. Сев не может объяснить действительно ли это так, или это игра его воображения. Все, что связано с Эванс до сих пор меняет его взгляды на происходящее. Он не злодей, не жертва обстоятельств, он просто человек, который раз за разом делает неправильный выбор, потому что хочет что-то доказать этому миру, себе. Хочет, но не может этого достичь.Severus Snape

— Дорогая Беллатрикс, — он лично считает, что начало ему удалось деликатнее некуда. Отличное великосветское начало. — какого драккла... — все же он староста и нужно сразу показать, как серьезны его намерения. Впрочем, так тоже нельзя, вспоминает Люциус... Нужно деликатно, она же Дама. Ну, будет. Через пару лет. Нужно как-то...без обвинений. — творится? Мы потеряли столько баллов, что я подумал: тебе, вероятно, нужна помощь...Lucius Malfoy

Взглядом провожает Беллу, которая идёт к нему и поднимает бровь, продолжая стоять на месте. Палочка в руках раздражала, как и то, что она ходит вокруг, а из-за своего роста, её причёска, которая похожа на его после полнолуния, мельтешила перед глазами. Инстинкт так и поддевал схватить малявку, у неё то палочка, но ему вспомнился пьяница на празднике Рождества, который устроил дебош и оказался в не лучшем положении, когда палочку сломали. А с ней тоже был очень уверен.Fenrir Greyback

Поведение Фенрира, явившегося к Темному Лорду, кажется ей вызывающим. Он не склоняет головы. Кивает, будто бы равному. Одно простенькое движение заставляет кровь Лестрейндж вскипеть. Она вспыльчива и фанатична, когда дело касается Повелителя. Рука невольно стискивает волшебную палочку. Одно единственное заклинание, самый простенький «Империус» и эта шавка будет не просто отвешивать поклоны – кинется лобызать ему ноги. Белла делает глубокий вздох. Не сейчас. Этот союз важен.Bellatrix Lestrange

Хотел бы на Гриффиндор? Нет. Конечно же нет! Почему я должен хотеть на Гриффиндор? Только потому что там Сириус? Я вообще-то не приложение к брату, чтобы следовать за ним по факультетам, коридорам и вообще по жизни. Да мы даже не похожи! Мать говорит, что он породы »британская… дворняга"… Я тебе точно говорю, его усыновили! Иначе с чего бы вдруг он попал на Гриффиндор. Это же немыслимо!Regulus Black

И конечно же, именно в тот вечер, когда Ремус решил заняться своим эссе, пока не пришло время ужина, библиотека переполнена студентами, хотя до экзаменов ещё несколько месяцев. Как будто бы на всех напала учебная лихорадка, или призрак Елены Рейвенкло, вспомнив девиз факультета своей матери, прокралась в головы учеников, поселив в них рвение к учёбе. Нашлись среди занявших столы между стеллажами и несколько парочек, больше занятых поцелуями и хихиканьем, как будто бы нельзя было найти места получше. Их Рем старался проходить как можно быстрее и как раз на одну такую наткнулся, сперва густо покраснев, а потом побледнев, когда встретился взглядом со старшекурсницей из Слизерина, оторвавшейся от своего приятеля.Remus Lupin

Джули едва ли могла уверенно сказать, что знала ее — видела пару раз на собраниях Ордена, и слышала слух, что недавняя выпускница факультета Рейвенкло обладает удивительным талантом находить общий язык с детьми, и вот же — свидетельство чуда без мановений волшебной палочкой. Кевин, задержав говорящий вместо него взгляд на волшебнице, перевел его обратно на Джули — и на нее уже смотрел не навсегда потерявший радость и наивность детства оставленный сын, а решившийся наконец-то открыть украшенный подарок ребенок, вдруг поверивший, что он правда для него.Julia White

Сколько лет прошло — мысли о родовых обязательствах наполняли душу матери горечью, которую не перебить ни сладким чаем ни теплой беседой, и смириться с тем, что предначертано — никогда не означало принять. Сколько бы наследных мальчиков не должно было пройти становление патриархами, сколько бы девочек не должно было лечь на заклание в русла кровных уз — легче к этому невозможно было относиться. Не той женщине, что сама оказалась предметом сделки двух ростовщиков, когда могла стать супругом патриарха Эйвери по собственной воле, если бы ей только дали выбор.Alexandrite Avery

В воцарившейся тишине купе собственные мысли стали ощутимо громче. Их с Александрит последняя встреча... обнадёживала. Однако, не так-то просто перечеркнуть годы отстранённости и прохлады, и где-то на задворках холодной души патриарха всё ещё скребло опасение — опасение, что она передумает, что всё вернётся к тому, что было до. Оно не вызывало тревоги, поскольку Морвран уже давно привык смиренно принимать правила, диктуемые супругой. Так же, как она когда-то приняла условия четырежды проклятого договора их предков.Morvran Avery

Поняла ли Александрит что-то про неё и Дамокла? Даже если и так, она была бы последним человеком, которого Роуз могла заподозрить в каких-то злых намерениях и распространении слухов. Глупо, наивно, они ведь толком незнакомы. Да и разница в их с Дамоклом чистоте крови могла возмутить аристократку сильнее, чем разница в их возрасте. Но Александрит не выглядела ни возмущённой, ни желающей вывести её на чистую воду. Даже подозревать эту волшебницу в подобных мыслях казалось оскорблением.Rose Harland

Как будто ей снова девять. Как будто она весь день носилась на улице. И как будто после такого длинного и занятого важными детскими делами дня она наконец вернулась домой, где ее очень-очень ждали. И если бы Марлин не знала, что подобной магии не существует, она непременно бы заподозрила семейство Уизли в каких-нибудь хитрых чарах, заставляющих каждого почувствовать себя самым желанным из гостей.Marlene McKinnon

Конец мелодии чуть участился вновь, дрогнул, окончившись величаво, как если бы само закатное солнце поставило точку перед тем, как полностью исчезнуть с горизонта, оставив после себя жаркие огненные крылья на небе. Прохладные пальцы Мальсибера, пока он играл, согрелись о мамино стучащее сердце, кривой рот исправился на легкую мягкую улыбку, а потемневшие до этого от злости зеленые глаза смягчились, сверкая при свечах как два изумруда.Anthony Mulciber

Они справятся. Джеймс и Лили справятся. Сириус и Марлин. Орден Феникса. Глядя на то, как две чистые любящие души выбирают друг друга, стоя перед алтарем, глядя влюбленно, счастливо, волнительно, у шафера жениха не остается и тени сомнений, что все они справятся. Солнечные лучи, бьющие золотом на брачующихся, словно благословляют их на долгую жизнь вместе. Сириус улыбается. Они этого заслужили.Sirius Black

Дирк бесконечно любил осень именно за конец октября. Редкое время в году, когда можно было объедаться тыквенными кексами, пить литрами тыквенный сок, и никто не осудит, ведь Хэллоуин на носу! Кругом тыквы, даже маги соревнуются друг с другом за право называться королём самой большой красивой тыквы, выросшей на их грядке. Вырезают страшные лица, проводят маскарады, украшают свои дома различными ужасами, показывая, что принимают зло, живущее в мире, как должное. И Крессвел с удовольствием этим пользовался, чтобы не отказывать себе в печеньках и сладостях, потому что не обожать тыкву — кощунство!Dirk Cresswell

Прямо сейчас куда больше восторга в ней вызвала реакция Авроры, ее глаза, полные счастья и обожания. С каплей недоверия Лавиния тоже шагнула ближе, чтоб разглядеть аликорна, но он и правда был цел. Будто бы новенький и только-только подаренный. Сестра тут же со своими обыкновенными искренностью и совершенно неконтролируемой, как и все ее остальные эмоции, благодарностью бросилась к Тони с криками. На этот раз счастливыми.Lavinia Mulciber

— Сладенький, ты всегда такой нервный? – спрашивает Раймонд бармена, наблюдая за улыбающимся Мальсибером. Он понимает, что этот сукин сын не испугался ни на секунду. Волшебник ему однозначно нравится. Рай жадно смотрит на Энтони, чувствуя, что, возможно, может не просто поконкурировать с ним в жестокости, но даже чему-то научиться.Raymond Pyrites

Сознание возвращается медленно и нехотя. Сначала появляются запахи, горькие зелья, чистое бельё и что-то травяное. Потом звуки, тиканье часов, шелест занавески, приглушённые шаги. Уиспер пытается открыть глаза, но свет немедленно вонзается в голову раскалённой иглой. Уиспер осторожно ощупывает языком зубы, все на месте, уже неплохо. Воспоминания возвращаются обрывками: Эван с битой, направленный в Тристиана удар, столкновение плечами, облупившаяся синяя краска и пустота. — Надеюсь, я красиво упал, — смеется он, но никто ему не отвечает. Только когда удается открыть глаза он видит сладости на прикроватной тумбочке и почему-то шоколадные лягушки с надписями на французском языке и французскими волшебниками.Whisper Whisp

Через несколько минут они будут идти с Уиспером по увешанному рождественскими гирляндами Косому переулку, кутаться в тёплые мантии, заглянут в небольшое кафе, из которого так уютно на всю улицу звучат рождественские песни Селестины Уорлок. Эван припомнит, что летом отсюда так же играла какая-то песня Селестины, и они точно так же решили заглянуть в это место свободным прохладным июльским вечером. — С Рождеством, — скажет он Уисперу совсем по-другому, не так, как говорил эти слова за столом в шумной редакции. И подумает про себя, что хотел бы встретить ещё сотню таких праздников, стукаясь кружками сливочного пива с корицей с этим парнем, и глядя в эти для одного него тёплые глаза. И загадает рождественское желание, о котором нельзя рассказывать вслух. Иначе не сбудется.Evan Rosier

Праздник догорел незаметно и быстро, как свеча за рождественским столом. Никто здесь не был настроен на работу, так что опоздание Эммы быстро забылось. Признаться, она и сама отвлеклась от своего приключения, проникнувшись духом Рождества, которое в редакции всё равно пахло не только хвоей и имбирём, но и чернилами, и свежими газетами.Emma Squiggle

Бесконечные вопросы, с его появлением вновь нахлынувшие бурным потоком, не могли пробиться сквозь ощущение его рядом, целого и невредимого. Его губы на её губах, вкус его поцелуев, который она так боялась навсегда забыть, тепло его тела под её руками и эти объятия, в которых всегда казалось, что безопаснее в мире места нет, всё это казалось сном. Одним из тех, в которых Дамокл возвращается к ней, целует, успокаивает, а потом она просыпается одна в пустой постели. — Ты же не снишься мне, правда?Rose Harland

С чем еще ей решительно повезло, так это со спутником жизни. Волшебница всегда удивлялась тому, как Артур виртуозно невилирует сложности. Как он шутит, когда она хочет плакать. Как поддерживает ее, замалчивая свои проблемы. Которых, Молли уверена, тоже было предостаточно. Артур ни разу за всю их совместную жизнь не заставил свою жену почувствовать себя не на своем месте; не давал ни единого повода для зарождения мысли о расставании или разводе. Он любил ее и это было очевидно даже слепому. А уж как он любил детей! Одна из причин, по которой Молли снова и снова готова была рожать — то самое плечо рядом, с которым ничего не страшно. Артур давал жене уверенность в том, что, что бы ни случилось, они со всем справятся.
Molly Weasley читать дальше

safe space design

Информация о пользователе

Привет, Гость! Войдите или зарегистрируйтесь.


Вы здесь » safe space design » Гостевой фолиант » тест примерки


тест примерки

Сообщений 1 страница 8 из 8

1

[hideprofile][html]
<div class="ybg-root">
    <div class="ybg-shop-head">
        <div class="ybg-shop-title">примерочная фонов</div>
        <div class="ybg-shop-subtitle">
            выберите фон для примерки или добавьте понравившиеся варианты в корзину
        </div>
    </div>
    <div class="ybg-layout">
        <div class="ybg-fitting">
            <div class="ybg-section-title">примерочная</div>
            <div class="ybg-fitting-box">
                <div id="ybg-fitting-room">
                    <div class="ybg-loading">
                        загружаю ваш профиль...
                    </div>
                </div>
            </div>
        </div>
        <div class="ybg-catalog">
            <div class="ybg-section-title">каталог</div>
            <div id="ybg-categories"></div>
        </div>
    </div>
    <div class="ybg-bottom">
        <div class="ybg-cart-info">
            <span class="ybg-cart-label">корзина</span>
            <span id="ybg-cart-total">0 галлеонов</span>
        </div>
        <button type="button" class="ybg-buy" onclick="ybgBuy()">
            оформить
        </button>
    </div>
    <div id="ybg-status"></div>
</div>

<style>
.ybg-root {
    --ybg-deep: var(--pun-bg, #3E1C34);
    --ybg-deep2: var(--sec-accent, #602748);
    --ybg-text: var(--text, #130912);
    --ybg-light: var(--tab-text, #b7ac9b);
    --ybg-accent: var(--navi-link, #F7921E);
    --ybg-sub: var(--podfor, rgba(62, 28, 52, .2));
    --ybg-border: var(--bord, rgba(0, 0, 0, .3));
    --ybg-dark-border: var(--dark-bord, #382536);
    --ybg-footer: var(--cfoot, #46293f);
    --ybg-radius: var(--radius, 10px);
    --ybg-radius2: var(--radius2, 15px);
    box-sizing: border-box;
    position: relative;
    width: 100%;
    margin: 0;
    padding: 18px;
    overflow: hidden;
    color: var(--ybg-text);
    font-family: var(--main-font, inter), Arial, sans-serif;
    background: linear-gradient(rgba(183, 172, 155, .84), rgba(183, 172, 155, .84)), var(--base-bg);
    border: 1px solid var(--ybg-border);
    border-radius: var(--ybg-radius2);
    box-shadow: 0 8px 22px rgba(19, 9, 18, .08);
}

.ybg-root .ybg-layout,
.ybg-root .ybg-fitting,
.ybg-root .ybg-fitting-box,
.ybg-root .ybg-catalog,
.ybg-root .ybg-category,
.ybg-root .ybg-category-head,
.ybg-root .ybg-items,
.ybg-root .ybg-item,
.ybg-root .ybg-item *,
.ybg-root .ybg-bottom,
.ybg-root .ybg-buy,
.ybg-root #ybg-status {
    box-sizing: border-box;
}

.ybg-root .ybg-shop-head {
    margin: -18px -18px 18px;
    padding: 15px 20px 14px;
    background: linear-gradient(rgba(62, 28, 52, .94), rgba(62, 28, 52, .94)), var(--navi-bg);
    border-bottom: 1px solid var(--ybg-dark-border);
}

.ybg-root .ybg-shop-title {
    color: var(--ybg-light);
    font-size: 18px;
    line-height: 1.1;
    text-transform: lowercase;
    letter-spacing: .03em;
}

.ybg-root .ybg-shop-subtitle {
    margin-top: 5px;
    color: rgba(183, 172, 155, .78);
    font-size: 10px;
    line-height: 1.4;
}

.ybg-root .ybg-layout {
    display: grid;
    grid-template-columns: 300px minmax(0, 1fr);
    gap: 18px;
    align-items: start;
}

.ybg-root .ybg-fitting,
.ybg-root .ybg-catalog {
    min-width: 0;
}

.ybg-root .ybg-section-title {
    margin-bottom: 7px;
    color: var(--ybg-deep);
    font-size: 11px;
    font-weight: 600;
    line-height: 1;
    text-transform: uppercase;
    letter-spacing: .08em;
}

.ybg-root .ybg-fitting-box {
    display: flex;
    align-items: flex-start;
    justify-content: center;
    width: 300px;
    min-height: 500px;
    padding: 10px;
    background: rgba(62, 28, 52, .08);
    border: 1px solid var(--ybg-border);
    border-radius: var(--ybg-radius2);
}

.ybg-root #ybg-fitting-room {
    display: flex;
    align-items: flex-start;
    justify-content: center;
    width: 100%;
}

.ybg-root .ybg-loading {
    width: 100%;
    padding: 70px 5px;
    color: rgba(19, 9, 18, .58);
    font-size: 10px;
    text-align: center;
}

.ybg-root #ybg-fitting-room .ybg-profile-shell {
    width: 100% !important;
    margin: 0 !important;
    padding: 0 !important;
}

.ybg-root #ybg-fitting-room .ybg-profile-post {
    width: 100% !important;
    margin: 0 !important;
    padding: 0 !important;
    background: transparent !important;
    border: 0 !important;
}

.ybg-root #ybg-fitting-room .ybg-profile-post > .container {
    display: flex !important;
    align-items: flex-start !important;
    justify-content: center !important;
    width: 100% !important;
    margin: 0 !important;
    padding: 0 !important;
    background: transparent !important;
}

.ybg-root #ybg-fitting-room .post-author {
    display: block !important;
    float: none !important;
    flex: none !important;
    margin: 0 auto !important;
    overflow: visible !important;
    isolation: isolate !important;
}

.ybg-root #ybg-fitting-room a {
    pointer-events: none !important;
}

.ybg-root #ybg-categories {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.ybg-root .ybg-category {
    margin: 0;
    padding: 0;
    overflow: hidden;
    background: rgba(62, 28, 52, .09);
    border: 1px solid var(--ybg-border);
    border-radius: var(--ybg-radius2);
}

.ybg-root .ybg-category-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    min-height: 38px;
    padding: 9px 12px;
    gap: 15px;
    background: var(--ybg-deep);
    border-bottom: 1px solid var(--ybg-dark-border);
}

.ybg-root .ybg-category-name {
    margin: 0;
    padding: 0;
    color: var(--ybg-light);
    font-size: 14px;
    font-weight: 500;
    line-height: 1;
    text-transform: lowercase;
    letter-spacing: .025em;
}

.ybg-root .ybg-category-price {
    padding: 4px 8px;
    color: var(--ybg-accent);
    font-size: 9px;
    line-height: 1;
    white-space: nowrap;
    background: rgba(0, 0, 0, .13);
    border-radius: 999px;
}

.ybg-root .ybg-items {
    display: grid;
    grid-template-columns: repeat(5, minmax(0, 1fr));
    gap: 8px;
    padding: 9px;
}

.ybg-root .ybg-item {
    position: relative;
    height: 44px;
    overflow: hidden;
    background: var(--ybg-footer);
    border: 1px solid var(--ybg-dark-border);
    border-radius: var(--ybg-radius);
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .04);
    cursor: pointer;
    transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease;
}

.ybg-root .ybg-item:hover {
    z-index: 3;
    transform: translateY(-2px);
    border-color: var(--ybg-deep2);
    box-shadow: 0 5px 12px rgba(19, 9, 18, .18);
}

.ybg-root .ybg-item.ybg-selected {
    border: 2px solid var(--ybg-accent);
    box-shadow: 0 0 0 2px rgba(247, 146, 30, .16);
}

.ybg-root .ybg-item-image {
    position: absolute;
    top: 0;
    left: 0;
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover !important;
    object-position: bottom !important;
    transition: transform .25s ease;
}

.ybg-root .ybg-item:hover .ybg-item-image {
    transform: scale(1.045);
}

.ybg-root .ybg-item::after {
    content: "";
    position: absolute;
    z-index: 2;
    right: 0;
    bottom: 0;
    left: 0;
    height: 40%;
    pointer-events: none;
    background: linear-gradient(to bottom, transparent, rgba(19, 9, 18, .48));
}

.ybg-root .ybg-item-number {
    position: absolute;
    z-index: 5;
    top: 6px;
    left: 6px;
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 21px;
    height: 21px;
    padding: 0 5px;
    color: var(--ybg-light);
    font-size: 9px;
    line-height: 1;
    background: rgba(62, 28, 52, .92);
    border: 1px solid rgba(183, 172, 155, .25);
    border-radius: 999px;
}

.ybg-root .ybg-cart-button {
    position: absolute;
    z-index: 6;
    right: 6px;
    bottom: 6px;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 27px;
    height: 23px;
    color: var(--ybg-light);
    font-size: 15px;
    line-height: 1;
    background: rgba(62, 28, 52, .90);
    border: 1px solid rgba(183, 172, 155, .28);
    border-radius: 7px;
    cursor: pointer;
    transition: background .18s ease, color .18s ease, border-color .18s ease;
}

.ybg-root .ybg-cart-button:hover {
    background: var(--ybg-deep2);
    border-color: var(--ybg-accent);
}

.ybg-root .ybg-item.ybg-in-cart .ybg-cart-button {
    color: var(--ybg-deep);
    background: var(--ybg-accent);
    border-color: var(--ybg-accent);
}

.ybg-root .ybg-cart-flag {
    position: absolute;
    z-index: 5;
    bottom: 7px;
    left: 6px;
    padding: 4px 7px;
    color: var(--ybg-light);
    font-size: 8px;
    line-height: 1;
    background: rgba(62, 28, 52, .88);
    border-radius: 6px;
}

.ybg-root .ybg-bottom {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    min-height: 50px;
    margin-top: 16px;
    padding: 9px 11px;
    gap: 16px;
    background: var(--ybg-sub);
    border: 1px solid var(--ybg-border);
    border-radius: var(--ybg-radius);
}

.ybg-root .ybg-cart-info {
    display: flex;
    align-items: baseline;
    gap: 7px;
}

.ybg-root .ybg-cart-label {
    color: rgba(19, 9, 18, .55);
    font-size: 9px;
    text-transform: uppercase;
    letter-spacing: .06em;
}

.ybg-root #ybg-cart-total {
    color: var(--ybg-deep);
    font-size: 11px;
    font-weight: 600;
}

.ybg-root .ybg-buy {
    min-width: 92px;
    padding: 7px 15px;
    color: var(--ybg-light);
    font-family: var(--main-font, inter), Arial, sans-serif;
    font-size: 9px;
    line-height: 1;
    text-transform: uppercase;
    letter-spacing: .06em;
    background: var(--ybg-deep);
    border: 1px solid var(--ybg-dark-border);
    border-radius: 8px;
    cursor: pointer;
    transition: background .18s ease, color .18s ease;
}

.ybg-root .ybg-buy:hover {
    color: var(--ybg-accent);
    background: var(--ybg-deep2);
}

.ybg-root #ybg-status {
    min-height: 14px;
    padding-top: 6px;
    color: var(--ybg-deep2);
    font-size: 9px;
    line-height: 1.3;
    text-align: right;
}

@media (max-width: 900px) {
    .ybg-root .ybg-layout {
        grid-template-columns: 280px minmax(0, 1fr);
    }

    .ybg-root .ybg-fitting-box {
        width: 280px;
    }

    .ybg-root .ybg-items {
        grid-template-columns: repeat(4, minmax(0, 1fr));
    }
}

@media (max-width: 720px) {
    .ybg-root {
        padding: 12px;
    }

    .ybg-root .ybg-shop-head {
        margin: -12px -12px 14px;
    }

    .ybg-root .ybg-layout {
        grid-template-columns: 1fr;
    }

    .ybg-root .ybg-fitting,
    .ybg-root .ybg-fitting-box {
        width: 100%;
    }

    .ybg-root .ybg-items {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    .ybg-root .ybg-item {
        height: 100px;
    }

    .ybg-root .ybg-bottom {
        justify-content: space-between;
    }
}
</style>

<script src="https://forumstatic.ru/files/001c/ba/0d/82391.js?v=18"></script>
[/html]

0

2

нужно что-то на красивое офомление

0

3

Свернутый текст

МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТАМНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТАМНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТАМНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТАМНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТАМНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТАМНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТАМНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТАМНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТАМНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТАМНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТАМНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТАМНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТАМНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТАМНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТАМНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТАМНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТАМНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТАМНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТАМНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТАМНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТАМНОГО ТЕКСТА МНОГО ТЕКСТА МНОГО ТЕКСТА

0

4

[html]<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Коллаж из 9 фотографий</title>
<style>
*{box-sizing:border-box}body{margin:0;padding:28px 16px;background:#f3eee5;color:#433c32;font:16px/1.5 system-ui,sans-serif}main{max-width:720px;margin:auto}h1{font:normal 32px Georgia,serif;margin:0 0 8px}p{margin:0 0 20px;color:#70685c}.controls{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:16px}button{font:inherit;cursor:pointer;border:1px solid #b8ad9a;border-radius:8px;padding:11px 16px;background:#fffaf2;color:inherit}button.primary{background:#514d3f;color:white;border-color:#514d3f}button:disabled{opacity:.5;cursor:default}button:focus-visible{outline:3px solid #ae7f36;outline-offset:3px}.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:0;width:100%;background:#ddd4c5;overflow:hidden}.cell{position:relative;display:block;width:100%;aspect-ratio:2/3;padding:0;border:0;border-radius:0;background:#e6ded1;overflow:hidden;color:#776d5e}.cell:nth-child(even){background:#dcd2c2}.cell canvas{display:block;width:100%;height:100%}.placeholder{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:5px;font-size:13px}.placeholder b{font-size:28px;font-weight:normal}.cell:focus-visible{outline-offset:-4px;z-index:1}.cell:hover{box-shadow:inset 0 0 0 2px #a18d6b}.cell.filled:hover:after{content:'Заменить';position:absolute;bottom:8px;left:50%;transform:translateX(-50%);background:#433c32d9;color:#fff;padding:4px 10px;border-radius:5px;font-size:12px}#status{min-height:24px;margin:12px 0;font-size:14px}.note{font-size:13px;margin-top:16px}@media(max-width:480px){body{padding:20px 12px}h1{font-size:27px}.controls button{flex:1;padding:10px 8px;font-size:14px}.placeholder{font-size:11px}}
</style>
</head>
<body>
<main>
<h1>Коллаж из 9 фотографий</h1>
<p>Выберите 9 изображений или нажмите на отдельное окно. Лишние края обрежутся по центру, пропорции сохранятся.</p>
<div class="controls">
  <button id="upload" type="button">Загрузить картинки</button>
  <button id="save" class="primary" type="button" disabled>Скачать PNG</button>
  <button id="clear" type="button" disabled>Очистить</button>
</div>
<input id="multiple" type="file" accept="image/*" multiple hidden>
<input id="single" type="file" accept="image/*" hidden>
<div id="grid" class="grid" aria-label="Коллаж: 3 ряда по 3 изображения"></div>
<div id="status" role="status" aria-live="polite">Загружено: 0 из 9</div>
<p class="note">Готовый коллаж: 1800 × 2700 пикселей, без рамок и промежутков. Изображения обрабатываются только в вашем браузере.</p>
</main>
<script>
(() => {
'use strict';
const $ = id => document.getElementById(id);
const slots = Array(9).fill(null);
const buttons = [];
const W = 600, H = 900;
let selected = 0, busy = false;
function refresh(message = '') {
  const count = slots.filter(Boolean).length;
  $('status').textContent = message || `Загружено: ${count} из 9`;
  $('save').disabled = busy || count !== 9;
  $('upload').disabled = busy;
  $('clear').disabled = busy || count === 0;
  buttons.forEach(button => button.disabled = busy);
}
for (let i = 0; i < 9; i++) {
  const button = document.createElement('button');
  button.type = 'button';
  button.className = 'cell';
  button.setAttribute('aria-label', `Загрузить изображение ${i + 1}`);
  button.innerHTML = `<span class="placeholder"><b>+</b>Фото ${i + 1}</span>`;
  button.addEventListener('click', () => {selected = i; $('single').click();});
  $('grid').append(button);
  buttons.push(button);
}
// Масштаб cover: заполняем окно без растяжения, обрезая лишнее по центру.
async function load(file, index) {
  const url = URL.createObjectURL(file);
  try {
   const img = new Image();
   await new Promise((resolve, reject) => {
    img.onload = resolve;
    img.onerror = () => reject(new Error('Не удалось открыть изображение'));
    img.src = url;
   });
   const canvas = document.createElement('canvas');
   canvas.width = W; canvas.height = H;
   const context = canvas.getContext('2d');
   if (!context) throw new Error('Canvas недоступен');
   context.fillStyle = '#ffffff'; context.fillRect(0, 0, W, H);
   context.imageSmoothingEnabled = true;
   context.imageSmoothingQuality = 'high';
   const scale = Math.max(W / img.naturalWidth, H / img.naturalHeight);
   const sw = W / scale, sh = H / scale;
   context.drawImage(img, (img.naturalWidth-sw)/2, (img.naturalHeight-sh)/2, sw, sh, 0, 0, W, H);
   slots[index] = canvas;
   buttons[index].replaceChildren(canvas);
   buttons[index].classList.add('filled');
   buttons[index].setAttribute('aria-label', `Заменить изображение ${index + 1}`);
  } finally {URL.revokeObjectURL(url);}
}
async function process(files, indexes, extra = '') {
  busy = true; refresh('Загружаем изображения…');
  let errors = 0;
  try {
   for (let i = 0; i < files.length; i++) {
    try {await load(files[i], indexes[i]);} catch {errors++;}
   }
  } finally {
   busy = false;
   refresh(`Загружено: ${slots.filter(Boolean).length} из 9.${errors ? ' Не удалось открыть файлов: ' + errors + '. Попробуйте JPG, PNG или WebP.' : ''}${extra}`);
  }
}
$('upload').addEventListener('click', () => $('multiple').click());
$('multiple').addEventListener('change', async event => {
  const files = Array.from(event.target.files); event.target.value = '';
  if (!files.length) return;
  const empty = slots.map((value, index) => value ? -1 : index).filter(index => index >= 0);
  // Полный набор заменяет весь коллаж; меньший набор заполняет свободные окна.
  const indexes = files.length >= 9 || !empty.length ? Array.from({length:9}, (_,i)=>i) : empty;
  const chosen = files.slice(0, indexes.length);
  await process(chosen, indexes, files.length > chosen.length ? ` Лишние файлы пропущены: ${files.length-chosen.length}.` : '');
});
$('single').addEventListener('change', async event => {
  const file = event.target.files[0]; event.target.value = '';
  if (file) await process([file], [selected]);
});
$('clear').addEventListener('click', () => {
  slots.fill(null);
  buttons.forEach((button,i) => {
   button.classList.remove('filled');
   button.innerHTML = `<span class="placeholder"><b>+</b>Фото ${i+1}</span>`;
   button.setAttribute('aria-label', `Загрузить изображение ${i+1}`);
  });
  refresh();
});
$('save').addEventListener('click', async () => {
  if (busy || slots.some(slot => !slot)) return;
  busy = true; refresh('Готовим PNG…');
  try {
   const canvas = document.createElement('canvas');
   canvas.width = W*3; canvas.height = H*3;
   const context = canvas.getContext('2d');
   slots.forEach((slot,i) => context.drawImage(slot, (i%3)*W, Math.floor(i/3)*H));
   const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
   if (!blob) throw new Error('Не удалось создать PNG');
   const url = URL.createObjectURL(blob);
   const link = document.createElement('a');
   link.href = url; link.download = 'collage-9-photos.png';
   document.body.append(link); link.click(); link.remove();
   setTimeout(() => URL.revokeObjectURL(url), 60000);
   busy = false; refresh('Коллаж готов. Скачивание PNG начато.');
  } catch {
   busy = false; refresh('Не удалось сохранить коллаж. Попробуйте ещё раз.');
  }
});
})();
</script>
</body>
</html>[/html]

0

5

[html]<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Коллаж из 9 фотографий</title>
<style>
*{box-sizing:border-box}body{margin:0;padding:28px 16px;background:#f3eee5;color:#433c32;font:16px/1.5 system-ui,sans-serif}main{max-width:720px;margin:auto}h1{font:normal 32px Georgia,serif;margin:0 0 8px}p{margin:0 0 20px;color:#70685c}.controls{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:16px}button{font:inherit;cursor:pointer;border:1px solid #b8ad9a;border-radius:8px;padding:11px 16px;background:#fffaf2;color:inherit}button.primary{background:#514d3f;color:white;border-color:#514d3f}button:disabled{opacity:.5;cursor:default}button:focus-visible{outline:3px solid #ae7f36;outline-offset:3px}.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:0;width:100%;background:#ddd4c5;overflow:hidden}.cell{position:relative;display:block;width:100%;aspect-ratio:2/3;padding:0;border:0;border-radius:0;background:#e6ded1;overflow:hidden;color:#776d5e}.cell:nth-child(even){background:#dcd2c2}.cell canvas{display:block;width:100%;height:100%}.placeholder{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:5px;font-size:13px}.placeholder b{font-size:28px;font-weight:normal}.cell:focus-visible{outline-offset:-4px;z-index:1}.cell:hover{box-shadow:inset 0 0 0 2px #a18d6b}.cell.filled:hover:after{content:'Двигайте фото';position:absolute;bottom:8px;left:50%;transform:translateX(-50%);background:#433c32d9;color:#fff;padding:4px 10px;border-radius:5px;font-size:12px}#status{min-height:24px;margin:12px 0;font-size:14px}.note{font-size:13px;margin-top:16px}@media(max-width:480px){body{padding:20px 12px}h1{font-size:27px}.controls button{flex:1;padding:10px 8px;font-size:14px}.placeholder{font-size:11px}}
.cell.filled{cursor:grab;touch-action:none;user-select:none}.cell.filled:active{cursor:grabbing}.cell.selected{outline:3px solid #a68045;outline-offset:-3px;z-index:1}#png{display:block;width:100%;height:auto}h2{font:normal 26px Georgia,serif;margin-top:30px}[hidden]{display:none!important}</style>
</head>
<body>
<main>
<h1>Коллаж из 9 фотографий</h1>
<p>Загрузите 9 картинок. Двигайте фотографии мышкой или пальцем внутри окон, чтобы выбрать нужный фрагмент. Пропорции сохраняются, пустых краёв не будет.</p>
<div class="controls">
  <button id="upload" type="button">Загрузить картинки</button>
  <button id="replace" type="button" disabled>Заменить выбранное фото</button>
  <button id="clear" type="button" disabled>Очистить</button>
</div>
<input id="multiple" type="file" accept="image/*" multiple hidden>
<input id="single" type="file" accept="image/*" hidden>
<div id="grid" class="grid" aria-label="Коллаж: 3 ряда по 3 изображения"></div>
<div id="status" role="status" aria-live="polite">Загружено: 0 из 9</div>
<section id="result" hidden>
  <h2>Готовый PNG</h2>
  <p>Нажмите на картинку ниже правой кнопкой мыши → «Сохранить картинку как…». На телефоне — удерживайте палец на картинке.</p>
  <img id="png" alt="Готовый коллаж из девяти фотографий">
</section>
<p class="note">Готовый коллаж (нажмите правой кнопкой - сохранить изображение как). Готовый коллаж загрузите в эту тему!</p>
</main>
<script>
(() => {
'use strict';
const $ = id => document.getElementById(id);
const slots = Array(9).fill(null), buttons = [];
const W = 600, H = 900;
let selected = -1, busy = false, drag = null;
function refresh(message = '') {
  const count = slots.filter(Boolean).length;
  $('status').textContent = message || `Загружено: ${count} из 9`;
  $('upload').disabled = busy;
  $('clear').disabled = busy || count === 0;
  $('replace').disabled = busy || selected < 0 || !slots[selected];
  buttons.forEach(button => button.disabled = busy);
}
function select(i) {
  selected = i;
  buttons.forEach((b,j) => b.classList.toggle('selected', j === i));
  refresh();
}
function draw(i) {
  const s = slots[i], ctx = s.canvas.getContext('2d');
  ctx.fillStyle = '#fff'; ctx.fillRect(0,0,W,H);
  ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high';
  ctx.drawImage(s.img, s.x,s.y,s.sw,s.sh,0,0,W,H);
}
function invalidate() {
  $('result').hidden = true;
  $('png').removeAttribute('src');
}
function compose() {
  if (slots.some(s => !s)) {invalidate(); return;}
  try {
   const canvas = document.createElement('canvas');
   canvas.width = W*3; canvas.height = H*3;
   const ctx = canvas.getContext('2d');
   slots.forEach((s,i) => ctx.drawImage(s.canvas,(i%3)*W,Math.floor(i/3)*H));
   // Настоящий PNG в элементе img: доступно стандартное меню сохранения.
   $('png').src = canvas.toDataURL('image/png');
   $('result').hidden = false;
  } catch {invalidate(); refresh('Не удалось собрать PNG. Попробуйте заменить слишком большие изображения.');}
}
function move(i, x, y) {
  const s = slots[i];
  s.x = Math.max(0, Math.min(s.img.naturalWidth-s.sw,x));
  s.y = Math.max(0, Math.min(s.img.naturalHeight-s.sh,y));
  draw(i);
}
for (let i=0;i<9;i++) {
  const b = document.createElement('button');
  b.type = 'button'; b.className = 'cell';
  b.setAttribute('aria-label', `Фото ${i+1}. Загрузить или выбрать. Стрелки перемещают фотографию.`);
  b.innerHTML = `<span class="placeholder"><b>+</b>Фото ${i+1}</span>`;
  b.addEventListener('click', () => {select(i); if (!slots[i]) $('single').click();});
  b.addEventListener('pointerdown', e => {
   if (busy || !slots[i] || e.button !== 0) return;
   select(i);
   const s = slots[i], rect = b.getBoundingClientRect();
   drag = {i,id:e.pointerId,px:e.clientX,py:e.clientY,x:s.x,y:s.y,rx:s.sw/rect.width,ry:s.sh/rect.height,moved:false};
   b.setPointerCapture(e.pointerId);
  });
  b.addEventListener('pointermove', e => {
   if (!drag || drag.i!==i || drag.id!==e.pointerId) return;
   const dx=e.clientX-drag.px, dy=e.clientY-drag.py;
   if (!drag.moved && Math.abs(dx)+Math.abs(dy)<3) return;
   if (!drag.moved) invalidate();
   drag.moved = true;
   move(i,drag.x-dx*drag.rx,drag.y-dy*drag.ry);
  });
  const finish = e => {
   if (!drag || drag.i!==i || drag.id!==e.pointerId) return;
   const moved = drag.moved; drag = null;
   if (b.hasPointerCapture(e.pointerId)) b.releasePointerCapture(e.pointerId);
   if (moved) compose();
  };
  b.addEventListener('pointerup',finish);
  b.addEventListener('pointercancel',finish);
  b.addEventListener('lostpointercapture',finish);
  b.addEventListener('keydown',e=>{
   if (!slots[i] || busy || !['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(e.key)) return;
   e.preventDefault(); select(i);
   const s=slots[i], step=(e.shiftKey?40:10)/s.scale;
   move(i,s.x+(e.key==='ArrowLeft'?step:e.key==='ArrowRight'?-step:0),s.y+(e.key==='ArrowUp'?step:e.key==='ArrowDown'?-step:0));
   compose();
  });
  $('grid').append(b); buttons.push(b);
}
async function load(file,i) {
  const url=URL.createObjectURL(file);
  try {
   const img=new Image();
   await new Promise((resolve,reject)=>{img.onload=resolve;img.onerror=reject;img.src=url;});
   const scale=Math.max(W/img.naturalWidth,H/img.naturalHeight);
   const sw=W/scale, sh=H/scale;
   const canvas=document.createElement('canvas'); canvas.width=W;canvas.height=H;
   slots[i]={img,canvas,scale,sw,sh,x:(img.naturalWidth-sw)/2,y:(img.naturalHeight-sh)/2};
   draw(i); buttons[i].replaceChildren(canvas);buttons[i].classList.add('filled');
  } finally {URL.revokeObjectURL(url);}
}
async function process(files,indexes,extra='') {
  busy=true;invalidate();refresh('Загружаем изображения…');let errors=0;
  try {
   for(let i=0;i<files.length;i++) {
    try {await load(files[i],indexes[i]);} catch {errors++;}
   }
  } finally {
   busy=false;
   refresh(`Загружено: ${slots.filter(Boolean).length} из 9.${errors?' Не удалось открыть файлов: '+errors+'. Попробуйте JPG, PNG или WebP.':''}${extra}`);
   compose();
  }
}
$('upload').addEventListener('click',()=>$('multiple').click());
$('replace').addEventListener('click',()=>$('single').click());
$('multiple').addEventListener('change',async e=>{
  const files=Array.from(e.target.files);e.target.value='';if(!files.length)return;
  const empty=slots.map((s,i)=>s?-1:i).filter(i=>i>=0);
  const indexes=files.length>=9 || !empty.length?Array.from({length:9},(_,i)=>i):empty;
  const chosen=files.slice(0,indexes.length);
  await process(chosen,indexes,files.length>chosen.length?` Лишние файлы пропущены: ${files.length-chosen.length}.`:'');
});
$('single').addEventListener('change',async e=>{
  const file=e.target.files[0];e.target.value='';if(file && selected>=0)await process([file],[selected]);
});
$('clear').addEventListener('click',()=>{
  slots.fill(null);selected=-1;invalidate();
  buttons.forEach((b,i)=>{b.classList.remove('filled','selected');b.innerHTML=`<span class="placeholder"><b>+</b>Фото ${i+1}</span>`;});
  refresh();
});
})();
</script>
</body>
</html>[/html]

0

6

[html]
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Конструктор коллажей</title>
<style>
*{box-sizing:border-box}
body{margin:0;padding:28px 16px;background:#f3eee5;color:#433c32;font:16px/1.5 system-ui,sans-serif}
main{max-width:760px;margin:auto}
h1{font:normal 32px Georgia,serif;margin:0 0 8px}
h2{font:normal 26px Georgia,serif;margin-top:30px}
p{margin:0 0 18px;color:#70685c}
.tabs{display:flex;gap:8px;margin:22px 0 18px;border-bottom:1px solid #cfc4b2}
.tab{border:0;border-radius:8px 8px 0 0;padding:11px 18px;background:#e6ded1;color:#665d50}
.tab.active{background:#514d3f;color:#fff}
.panel[hidden],[hidden]{display:none!important}
.controls,
button,select{font:inherit;border:1px solid #b8ad9a;border-radius:8px;padding:10px 14px;background:#fffaf2;color:inherit}
button{cursor:pointer}
button:disabled{opacity:.5;cursor:default}
button:focus-visible{outline:3px solid #ae7f36;outline-offset:3px}
.stage{position:relative}
.grid{display:grid;gap:0;width:100%;background:#ddd4c5;overflow:hidden}
.grid-nine{grid-template-columns:repeat(3,minmax(0,1fr))}
.grid-eight{grid-template-columns:repeat(2,minmax(0,1fr))}
.cell{position:relative;display:block;width:100%;padding:0;border:0;border-radius:0;background:#e6ded1;overflow:hidden;color:#776d5e}
.grid-nine .cell{aspect-ratio:2/3}
.grid-eight .cell{aspect-ratio:10/7}
.cell:nth-child(even){background:#dcd2c2}
.cell canvas{display:block;width:100%;height:100%}
.placeholder{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:5px;font-size:13px}
.placeholder b{font-size:28px;font-weight:normal}
.cell.filled{cursor:grab;touch-action:none;user-select:none}
.cell.filled:active{cursor:grabbing}
.cell.selected{outline:3px solid #a68045;outline-offset:-3px;z-index:2}
.cell.filled:hover:after{content:'Двигайте фото';position:absolute;bottom:8px;left:50%;transform:translateX(-50%);background:#433c32d9;color:#fff;padding:4px 10px;border-radius:5px;font-size:12px;white-space:nowrap}

.status{min-height:22px;margin:10px 0;font-size:13px}
.result img{display:block;width:100%;height:auto}
.note{font-size:13px;margin-top:16px}
@media(max-width:480px){
body{padding:20px 12px}h1{font-size:27px}.controls button{flex:1;padding:9px 7px;font-size:13px}
.tab{flex:1;padding:10px 8px}.placeholder{font-size:11px}
}
</style>
</head>
<body>
<main>
<h1>Конструктор коллажей</h1>
<p>Загружайте фотографии и двигайте их мышкой или пальцем внутри окон. Готовый коллаж появляется как обычная PNG-картинка — её можно сохранить через правую кнопку мыши.</p>

<div class="tabs" role="tablist">
<button class="tab active" data-tab="nine" role="tab" aria-selected="true">9 фото · 3×3</button>
<button class="tab" data-tab="eight" role="tab" aria-selected="false">8 фото · 2×4</button>
</div>

<section class="panel" id="panel-nine">
<p><b>Вкладка 1:</b> девять вертикальных окон 3×3.</p>
<div class="editor" data-editor="nine"></div>
</section>

<section class="panel" id="panel-eight" hidden>
<p><b>Вкладка 2:</b> восемь широких окон 2×4 — как на присланном примере.</p>
<div class="editor" data-editor="eight"></div>
</section>

<p class="note">Готовый коллаж (нажмите правой кнопкой - сохранить изображение как). Готовый коллаж загрузите в эту тему!</p>
</main>

<script>
(() => {
'use strict';
const $ = (s,root=document)=>root.querySelector(s);
const $$ = (s,root=document)=>[...root.querySelectorAll(s)];

const configs = {
nine:{count:9,cols:3,W:600,H:900,out:'1800 × 2700'},
eight:{count:8,cols:2,W:1000,H:700,out:'2000 × 2800'}
};
const editors = {};

function editorMarkup(cfg){
return `
<div class="controls">
  <button class="upload" type="button">Загрузить картинки</button>
  <button class="replace" type="button" disabled>Заменить выбранное фото</button>
  <button class="clear" type="button" disabled>Очистить</button>
</div>
<input class="multiple" type="file" accept="image/*" multiple hidden>
<input class="single" type="file" accept="image/*" hidden>
<div class="stage">
  <div class="grid grid-${cfg.count===9?'nine':'eight'}"></div>
</div>
<div class="status">Загружено: 0 из ${cfg.count}</div>
<section class="result" hidden>
  <h2>Готовый PNG</h2>
  <p>Правый клик по картинке → «Сохранить картинку как…». На телефоне — удерживайте палец.</p>
  <img class="png" alt="Готовый коллаж">
  <p class="note">Размер PNG: ${cfg.out} пикселей.</p>
</section>`;
}

function makeEditor(root,key){
const cfg=configs[key];
root.innerHTML=editorMarkup(cfg);
const slots=Array(cfg.count).fill(null), buttons=[];
let selected=-1,busy=false,drag=null;

const q=s=>$(s,root);
function refresh(msg=''){
  const count=slots.filter(Boolean).length;
  q('.status').textContent=msg||`Загружено: ${count} из ${cfg.count}`;
  q('.upload').disabled=busy;
  q('.clear').disabled=busy||count===0;
  q('.replace').disabled=busy||selected<0||!slots[selected];
  buttons.forEach(b=>b.disabled=busy);
}
function select(i){
  selected=i;buttons.forEach((b,j)=>b.classList.toggle('selected',j===i));refresh();
}
function invalidate(){q('.result').hidden=true;q('.png').removeAttribute('src')}
function draw(i){
  const s=slots[i],ctx=s.canvas.getContext('2d');
  ctx.fillStyle='#fff';ctx.fillRect(0,0,cfg.W,cfg.H);
  ctx.imageSmoothingEnabled=true;ctx.imageSmoothingQuality='high';
  ctx.drawImage(s.img,s.x,s.y,s.sw,s.sh,0,0,cfg.W,cfg.H);
}
function move(i,x,y){
  const s=slots[i];
  s.x=Math.max(0,Math.min(s.img.naturalWidth-s.sw,x));
  s.y=Math.max(0,Math.min(s.img.naturalHeight-s.sh,y));
  draw(i);
}
function compose(){
  if(slots.some(s=>!s)){invalidate();return}
  try{
   const c=document.createElement('canvas');
   const rows=Math.ceil(cfg.count/cfg.cols);
   c.width=cfg.W*cfg.cols;c.height=cfg.H*rows;
   const ctx=c.getContext('2d');
   slots.forEach((s,i)=>ctx.drawImage(s.canvas,(i%cfg.cols)*cfg.W,Math.floor(i/cfg.cols)*cfg.H));
   q('.png').src=c.toDataURL('image/png');
   q('.result').hidden=false;
  }catch(e){invalidate();refresh('Не удалось собрать PNG. Попробуйте заменить слишком большие изображения.')}
}
async function load(file,i){
  const url=URL.createObjectURL(file);
  try{
   const img=new Image();
   await new Promise((ok,no)=>{img.onload=ok;img.onerror=no;img.src=url});
   const scale=Math.max(cfg.W/img.naturalWidth,cfg.H/img.naturalHeight);
   const sw=cfg.W/scale,sh=cfg.H/scale;
   const canvas=document.createElement('canvas');canvas.width=cfg.W;canvas.height=cfg.H;
   slots[i]={img,canvas,scale,sw,sh,x:(img.naturalWidth-sw)/2,y:(img.naturalHeight-sh)/2};
   draw(i);buttons[i].replaceChildren(canvas);buttons[i].classList.add('filled');
  }finally{URL.revokeObjectURL(url)}
}
async function process(files,indexes,extra=''){
  busy=true;invalidate();refresh('Загружаем изображения…');let errors=0;
  try{for(let i=0;i<files.length;i++){try{await load(files[i],indexes[i])}catch(e){errors++}}}
  finally{
   busy=false;refresh(`Загружено: ${slots.filter(Boolean).length} из ${cfg.count}.${errors?' Ошибок: '+errors+'.':''}${extra}`);compose();
  }
}

for(let i=0;i<cfg.count;i++){
  const b=document.createElement('button');b.type='button';b.className='cell';
  b.innerHTML=`<span class="placeholder"><b>+</b>Фото ${i+1}</span>`;
  b.addEventListener('click',()=>{select(i);if(!slots[i])q('.single').click()});
  b.addEventListener('pointerdown',e=>{
   if(busy||!slots[i]||e.button!==0)return;select(i);
   const s=slots[i],r=b.getBoundingClientRect();
   drag={i,id:e.pointerId,px:e.clientX,py:e.clientY,x:s.x,y:s.y,rx:s.sw/r.width,ry:s.sh/r.height,moved:false};
   b.setPointerCapture(e.pointerId);
  });
  b.addEventListener('pointermove',e=>{
   if(!drag||drag.i!==i||drag.id!==e.pointerId)return;
   const dx=e.clientX-drag.px,dy=e.clientY-drag.py;
   if(!drag.moved&&Math.abs(dx)+Math.abs(dy)<3)return;
   if(!drag.moved)invalidate();drag.moved=true;move(i,drag.x-dx*drag.rx,drag.y-dy*drag.ry);
  });
  const finish=e=>{
   if(!drag||drag.i!==i||drag.id!==e.pointerId)return;
   const moved=drag.moved;drag=null;
   if(b.hasPointerCapture(e.pointerId))b.releasePointerCapture(e.pointerId);
   if(moved)compose();
  };
  b.addEventListener('pointerup',finish);b.addEventListener('pointercancel',finish);b.addEventListener('lostpointercapture',finish);
  q('.grid').append(b);buttons.push(b);
}

q('.upload').onclick=()=>q('.multiple').click();
q('.replace').onclick=()=>q('.single').click();
q('.multiple').onchange=async e=>{
  const files=[...e.target.files];e.target.value='';if(!files.length)return;
  const empty=slots.map((s,i)=>s?-1:i).filter(i=>i>=0);
  const indexes=files.length>=cfg.count||!empty.length?Array.from({length:cfg.count},(_,i)=>i):empty;
  const chosen=files.slice(0,indexes.length);
  await process(chosen,indexes,files.length>chosen.length?` Лишние файлы пропущены: ${files.length-chosen.length}.`:'');
};
q('.single').onchange=async e=>{const f=e.target.files[0];e.target.value='';if(f&&selected>=0)await process([f],[selected])};
q('.clear').onclick=()=>{
  slots.fill(null);selected=-1;invalidate();
  buttons.forEach((b,i)=>{b.classList.remove('filled','selected');b.innerHTML=`<span class="placeholder"><b>+</b>Фото ${i+1}</span>`});refresh();
};
return {compose};
}

$$('.editor').forEach(el=>editors[el.dataset.editor]=makeEditor(el,el.dataset.editor));
$$('.tab').forEach(tab=>tab.onclick=()=>{
$$('.tab').forEach(t=>{const on=t===tab;t.classList.toggle('active',on);t.setAttribute('aria-selected',on)});
$$('.panel').forEach(p=>p.hidden=p.id!==`panel-${tab.dataset.tab}`);
});
})();
</script>
</body>
</html>[/html]

0

7

[html]
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Конструктор коллажей</title>
<style>
*{box-sizing:border-box}
body{margin:0;padding:28px 16px;background:#f3eee5;color:#433c32;font:16px/1.5 system-ui,sans-serif}
main{max-width:760px;margin:auto}
h1{font:normal 32px Georgia,serif;margin:0 0 8px}
h2{font:normal 26px Georgia,serif;margin-top:30px}
p{margin:0 0 18px;color:#70685c}
.tabs{display:flex;gap:8px;margin:22px 0 18px;border-bottom:1px solid #cfc4b2}
.tab{border:0;border-radius:8px 8px 0 0;padding:11px 18px;background:#e6ded1;color:#665d50}
.tab.active{background:#514d3f;color:#fff}
.panel[hidden],[hidden]{display:none!important}
.controls,
button,select{font:inherit;border:1px solid #b8ad9a;border-radius:8px;padding:10px 14px;background:#fffaf2;color:inherit}
button{cursor:pointer}
button:disabled{opacity:.5;cursor:default}
button:focus-visible{outline:3px solid #ae7f36;outline-offset:3px}
.stage{position:relative}
.grid{display:grid;gap:0;width:100%;background:#ddd4c5;overflow:hidden}
.grid-nine{grid-template-columns:repeat(3,minmax(0,1fr))}
.grid-eight{grid-template-columns:repeat(2,minmax(0,1fr))}
.cell{position:relative;display:block;width:100%;padding:0;border:0;border-radius:0;background:#e6ded1;overflow:hidden;color:#776d5e}
.grid-nine .cell{aspect-ratio:2/3}
.grid-eight .cell{aspect-ratio:10/7}
.cell:nth-child(even){background:#dcd2c2}
.cell canvas{display:block;width:100%;height:100%}
.placeholder{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:5px;font-size:13px}
.placeholder b{font-size:28px;font-weight:normal}
.cell.filled{cursor:grab;touch-action:none;user-select:none}
.cell.filled:active{cursor:grabbing}
.cell.selected{outline:3px solid #a68045;outline-offset:-3px;z-index:2}
.cell.filled:hover:after{content:'Двигайте фото';position:absolute;bottom:8px;left:50%;transform:translateX(-50%);background:#433c32d9;color:#fff;padding:4px 10px;border-radius:5px;font-size:12px;white-space:nowrap}

.status{min-height:22px;margin:10px 0;font-size:13px}
.result img{display:block;width:100%;height:auto}
.note{font-size:13px;margin-top:16px}
@media(max-width:480px){
body{padding:20px 12px}h1{font-size:27px}.controls button{flex:1;padding:9px 7px;font-size:13px}
.tab{flex:1;padding:10px 8px}.placeholder{font-size:11px}
}

.free-toolbar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin:0 0 12px}
.free-toolbar label{display:flex;align-items:center;gap:8px}
.free-toolbar input[type=range]{width:170px}
.free-stage{position:relative;width:100%;aspect-ratio:5/7;background-color:#fff;background-image:linear-gradient(45deg,#eee 25%,transparent 25%),linear-gradient(-45deg,#eee 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#eee 75%),linear-gradient(-45deg,transparent 75%,#eee 75%);background-size:24px 24px;background-position:0 0,0 12px,12px -12px,-12px 0;border:1px solid #cfc4b2;overflow:hidden;touch-action:none;user-select:none}
.free-stage canvas{display:block;width:100%;height:100%;cursor:default}
.free-stage.landscape{aspect-ratio:7/5}.free-stage.has-selection canvas{cursor:grab}
.free-stage.dragging canvas{cursor:grabbing}
.free-help{font-size:13px;color:#776d5e;margin:8px 0 16px}
.free-count{font-size:13px;margin:8px 0}
@media(max-width:480px){
.free-toolbar button{padding:8px 9px;font-size:12px}
.free-toolbar label{width:100%}
.free-toolbar input[type=range]{flex:1}
}
</style>
</head>
<body>
<main>
<h1>Конструктор коллажей</h1>
<p>Загружайте фотографии и двигайте их мышкой или пальцем внутри окон. Готовый коллаж появляется как обычная PNG-картинка — её можно сохранить через правую кнопку мыши.</p>

<div class="tabs" role="tablist">
<button class="tab active" data-tab="nine" role="tab" aria-selected="true">9 фото · 3×3</button>
<button class="tab" data-tab="eight" role="tab" aria-selected="false">8 фото · 2×4</button>
<button class="tab" data-tab="free" role="tab" aria-selected="false">Свободный коллаж</button>
</div>

<section class="panel" id="panel-nine">
<p><b>Вкладка 1:</b> девять вертикальных окон 3×3.</p>
<div class="editor" data-editor="nine"></div>
</section>

<section class="panel" id="panel-eight" hidden>
<p><b>Вкладка 2:</b> восемь широких окон 2×4 — как на присланном примере.</p>
<div class="editor" data-editor="eight"></div>
</section>

<section class="panel" id="panel-free" hidden>
<p><b>Свободный коллаж:</b> прозрачное поле двух размеров: вертикальное 2000 × 2800 px или горизонтальное 2800 × 2000 px. Можно загрузить до 30 JPEG/PNG, свободно двигать их, менять масштаб, поворот и порядок слоёв.</p>

<div class="free-toolbar">
  <label>Размер:
   <select id="free-size">
    <option value="portrait">Вертикальный · 2000 × 2800</option>
    <option value="landscape">Горизонтальный · 2800 × 2000</option>
   </select>
  </label>
  <button id="free-upload" type="button">Добавить картинки</button>
  <input id="free-files" type="file" accept="image/jpeg,image/png" multiple hidden>
  <button id="free-delete" type="button" disabled>Удалить выбранную</button>
  <button id="free-clear" type="button" disabled>Очистить всё</button>
</div>

<div class="free-toolbar">
  <label>Масштаб
   <input id="free-scale" type="range" min="5" max="400" value="100" step="1" disabled>
   <span id="free-scale-value">100%</span>
  </label>
  <label>Поворот
   <input id="free-rotate" type="range" min="-180" max="180" value="0" step="1" disabled>
   <span id="free-rotate-value">0°</span>
  </label>
  <button id="free-rotate-left" type="button" disabled>↶ 15°</button>
  <button id="free-rotate-right" type="button" disabled>↷ 15°</button>
  <button id="free-back" type="button" disabled>На слой назад</button>
  <button id="free-front" type="button" disabled>На слой вперёд</button>
  <button id="free-bottom" type="button" disabled>На самый задний</button>
  <button id="free-top" type="button" disabled>На самый передний</button>
</div>

<div class="free-count" id="free-count">Загружено: 0 из 30</div>
<div class="free-stage" id="free-stage">
  <canvas id="free-canvas" width="2000" height="2800"></canvas>
</div>
<p class="free-help">Кликните по картинке, чтобы выбрать её. Перетаскивайте мышкой или пальцем. Масштаб и поворот меняются ползунками. Выбранная картинка в редакторе отмечена пунктиром — в итоговый PNG пунктир не попадает. Пустые участки сохраняются прозрачными.</p>

<section class="result" id="free-result" hidden>
  <h2>Готовый PNG</h2>
  <p>Правый клик по картинке → «Сохранить картинку как…». На телефоне — удерживайте палец.</p>
  <img id="free-png" alt="Готовый свободный коллаж">
  <p class="note" id="free-output-size">Размер PNG: 2000 × 2800 пикселей.</p>
</section>
</section>

<p class="note">Фотографии обрабатываются только в браузере. Вкладки хранят свои изображения отдельно.</p>
</main>

<script>
(() => {
'use strict';
const $ = (s,root=document)=>root.querySelector(s);
const $$ = (s,root=document)=>[...root.querySelectorAll(s)];

const configs = {
nine:{count:9,cols:3,W:600,H:900,out:'1800 × 2700'},
eight:{count:8,cols:2,W:1000,H:700,out:'2000 × 2800'}
};
const editors = {};

function editorMarkup(cfg){
return `
<div class="controls">
  <button class="upload" type="button">Загрузить картинки</button>
  <button class="replace" type="button" disabled>Заменить выбранное фото</button>
  <button class="clear" type="button" disabled>Очистить</button>
</div>
<input class="multiple" type="file" accept="image/*" multiple hidden>
<input class="single" type="file" accept="image/*" hidden>
<div class="stage">
  <div class="grid grid-${cfg.count===9?'nine':'eight'}"></div>
</div>
<div class="status">Загружено: 0 из ${cfg.count}</div>
<section class="result" hidden>
  <h2>Готовый PNG</h2>
  <p>Правый клик по картинке → «Сохранить картинку как…». На телефоне — удерживайте палец.</p>
  <img class="png" alt="Готовый коллаж">
  <p class="note">Размер PNG: ${cfg.out} пикселей.</p>
</section>`;
}

function makeEditor(root,key){
const cfg=configs[key];
root.innerHTML=editorMarkup(cfg);
const slots=Array(cfg.count).fill(null), buttons=[];
let selected=-1,busy=false,drag=null;

const q=s=>$(s,root);
function refresh(msg=''){
  const count=slots.filter(Boolean).length;
  q('.status').textContent=msg||`Загружено: ${count} из ${cfg.count}`;
  q('.upload').disabled=busy;
  q('.clear').disabled=busy||count===0;
  q('.replace').disabled=busy||selected<0||!slots[selected];
  buttons.forEach(b=>b.disabled=busy);
}
function select(i){
  selected=i;buttons.forEach((b,j)=>b.classList.toggle('selected',j===i));refresh();
}
function invalidate(){q('.result').hidden=true;q('.png').removeAttribute('src')}
function draw(i){
  const s=slots[i],ctx=s.canvas.getContext('2d');
  ctx.fillStyle='#fff';ctx.fillRect(0,0,cfg.W,cfg.H);
  ctx.imageSmoothingEnabled=true;ctx.imageSmoothingQuality='high';
  ctx.drawImage(s.img,s.x,s.y,s.sw,s.sh,0,0,cfg.W,cfg.H);
}
function move(i,x,y){
  const s=slots[i];
  s.x=Math.max(0,Math.min(s.img.naturalWidth-s.sw,x));
  s.y=Math.max(0,Math.min(s.img.naturalHeight-s.sh,y));
  draw(i);
}
function compose(){
  if(slots.some(s=>!s)){invalidate();return}
  try{
   const c=document.createElement('canvas');
   const rows=Math.ceil(cfg.count/cfg.cols);
   c.width=cfg.W*cfg.cols;c.height=cfg.H*rows;
   const ctx=c.getContext('2d');
   slots.forEach((s,i)=>ctx.drawImage(s.canvas,(i%cfg.cols)*cfg.W,Math.floor(i/cfg.cols)*cfg.H));
   q('.png').src=c.toDataURL('image/png');
   q('.result').hidden=false;
  }catch(e){invalidate();refresh('Не удалось собрать PNG. Попробуйте заменить слишком большие изображения.')}
}
async function load(file,i){
  const url=URL.createObjectURL(file);
  try{
   const img=new Image();
   await new Promise((ok,no)=>{img.onload=ok;img.onerror=no;img.src=url});
   const scale=Math.max(cfg.W/img.naturalWidth,cfg.H/img.naturalHeight);
   const sw=cfg.W/scale,sh=cfg.H/scale;
   const canvas=document.createElement('canvas');canvas.width=cfg.W;canvas.height=cfg.H;
   slots[i]={img,canvas,scale,sw,sh,x:(img.naturalWidth-sw)/2,y:(img.naturalHeight-sh)/2};
   draw(i);buttons[i].replaceChildren(canvas);buttons[i].classList.add('filled');
  }finally{URL.revokeObjectURL(url)}
}
async function process(files,indexes,extra=''){
  busy=true;invalidate();refresh('Загружаем изображения…');let errors=0;
  try{for(let i=0;i<files.length;i++){try{await load(files[i],indexes[i])}catch(e){errors++}}}
  finally{
   busy=false;refresh(`Загружено: ${slots.filter(Boolean).length} из ${cfg.count}.${errors?' Ошибок: '+errors+'.':''}${extra}`);compose();
  }
}

for(let i=0;i<cfg.count;i++){
  const b=document.createElement('button');b.type='button';b.className='cell';
  b.innerHTML=`<span class="placeholder"><b>+</b>Фото ${i+1}</span>`;
  b.addEventListener('click',()=>{select(i);if(!slots[i])q('.single').click()});
  b.addEventListener('pointerdown',e=>{
   if(busy||!slots[i]||e.button!==0)return;select(i);
   const s=slots[i],r=b.getBoundingClientRect();
   drag={i,id:e.pointerId,px:e.clientX,py:e.clientY,x:s.x,y:s.y,rx:s.sw/r.width,ry:s.sh/r.height,moved:false};
   b.setPointerCapture(e.pointerId);
  });
  b.addEventListener('pointermove',e=>{
   if(!drag||drag.i!==i||drag.id!==e.pointerId)return;
   const dx=e.clientX-drag.px,dy=e.clientY-drag.py;
   if(!drag.moved&&Math.abs(dx)+Math.abs(dy)<3)return;
   if(!drag.moved)invalidate();drag.moved=true;move(i,drag.x-dx*drag.rx,drag.y-dy*drag.ry);
  });
  const finish=e=>{
   if(!drag||drag.i!==i||drag.id!==e.pointerId)return;
   const moved=drag.moved;drag=null;
   if(b.hasPointerCapture(e.pointerId))b.releasePointerCapture(e.pointerId);
   if(moved)compose();
  };
  b.addEventListener('pointerup',finish);b.addEventListener('pointercancel',finish);b.addEventListener('lostpointercapture',finish);
  q('.grid').append(b);buttons.push(b);
}

q('.upload').onclick=()=>q('.multiple').click();
q('.replace').onclick=()=>q('.single').click();
q('.multiple').onchange=async e=>{
  const files=[...e.target.files];e.target.value='';if(!files.length)return;
  const empty=slots.map((s,i)=>s?-1:i).filter(i=>i>=0);
  const indexes=files.length>=cfg.count||!empty.length?Array.from({length:cfg.count},(_,i)=>i):empty;
  const chosen=files.slice(0,indexes.length);
  await process(chosen,indexes,files.length>chosen.length?` Лишние файлы пропущены: ${files.length-chosen.length}.`:'');
};
q('.single').onchange=async e=>{const f=e.target.files[0];e.target.value='';if(f&&selected>=0)await process([f],[selected])};
q('.clear').onclick=()=>{
  slots.fill(null);selected=-1;invalidate();
  buttons.forEach((b,i)=>{b.classList.remove('filled','selected');b.innerHTML=`<span class="placeholder"><b>+</b>Фото ${i+1}</span>`});refresh();
};
return {compose};
}

$$('.editor').forEach(el=>editors[el.dataset.editor]=makeEditor(el,el.dataset.editor));

function makeFreeEditor(){
const canvas=document.getElementById('free-canvas');
const sizeChoice=document.getElementById('free-size');
const ctx=canvas.getContext('2d');
const stage=document.getElementById('free-stage');
const files=document.getElementById('free-files');
const scale=document.getElementById('free-scale');
const scaleValue=document.getElementById('free-scale-value');
const rotate=document.getElementById('free-rotate');
const rotateValue=document.getElementById('free-rotate-value');
const rotateLeft=document.getElementById('free-rotate-left');
const rotateRight=document.getElementById('free-rotate-right');
const countEl=document.getElementById('free-count');
const png=document.getElementById('free-png');
const result=document.getElementById('free-result');
const upload=document.getElementById('free-upload');
const del=document.getElementById('free-delete');
const clear=document.getElementById('free-clear');
const back=document.getElementById('free-back');
const front=document.getElementById('free-front');
const bottom=document.getElementById('free-bottom');
const top=document.getElementById('free-top');

const items=[];
let selected=null;
let drag=null;
let raf=0;
const MAX=30;

function selectedIndex(){return selected?items.indexOf(selected):-1}
function setCanvasSize(mode){
  const oldW=canvas.width,oldH=canvas.height;
  const newW=mode==='landscape'?2800:2000;
  const newH=mode==='landscape'?2000:2800;
  if(oldW===newW&&oldH===newH)return;

  // Keep the relative positions and apparent sizes of existing layers.
  const sx=newW/oldW,sy=newH/oldH;
  const uniform=Math.min(sx,sy);
  for(const it of items){
   it.x*=sx;
   it.y*=sy;
   it.baseW*=uniform;
   it.baseH*=uniform;
  }

  canvas.width=newW;
  canvas.height=newH;
  stage.classList.toggle('landscape',mode==='landscape');
  document.getElementById('free-output-size').textContent=`Размер PNG: ${newW} × ${newH} пикселей.`;
  draw(true);
  if(items.length)makePng();
  else result.hidden=true;
}
function updateControls(){
  const i=selectedIndex(), has=i>=0;
  countEl.textContent=`Загружено: ${items.length} из ${MAX}`;
  upload.disabled=items.length>=MAX;
  del.disabled=!has;
  clear.disabled=items.length===0;
  scale.disabled=!has;
  rotate.disabled=!has;
  rotateLeft.disabled=!has;
  rotateRight.disabled=!has;
  back.disabled=!has||i===0;
  bottom.disabled=!has||i===0;
  front.disabled=!has||i===items.length-1;
  top.disabled=!has||i===items.length-1;
  stage.classList.toggle('has-selection',has);
  if(has){
   scale.value=Math.round(selected.scale*100);
   scaleValue.textContent=Math.round(selected.scale*100)+'%';
   rotate.value=Math.round(selected.rotation*180/Math.PI);
   rotateValue.textContent=Math.round(selected.rotation*180/Math.PI)+'°';
  }else{
   scale.value=100;scaleValue.textContent='100%';
   rotate.value=0;rotateValue.textContent='0°';
  }
}
function draw(showSelection=true){
  ctx.clearRect(0,0,canvas.width,canvas.height);
  for(const it of items){
   const w=it.baseW*it.scale,h=it.baseH*it.scale;
   ctx.save();
   ctx.translate(it.x,it.y);
   ctx.rotate(it.rotation);
   ctx.drawImage(it.img,-w/2,-h/2,w,h);
   ctx.restore();
  }
  if(showSelection&&selected){
   const w=selected.baseW*selected.scale,h=selected.baseH*selected.scale;
   ctx.save();
   ctx.translate(selected.x,selected.y);
   ctx.rotate(selected.rotation);
   ctx.strokeStyle='#111';
   ctx.lineWidth=Math.max(3,canvas.width/500);
   ctx.setLineDash([18,12]);
   ctx.strokeRect(-w/2,-h/2,w,h);
   ctx.restore();
  }
}
function makePng(){
  draw(false);
  try{
   png.src=canvas.toDataURL('image/png');
   result.hidden=items.length===0;
  }catch(e){result.hidden=true}
  draw(true);
}
function schedulePng(){
  clearTimeout(schedulePng.t);
  schedulePng.t=setTimeout(makePng,80);
}
function point(e){
  const r=canvas.getBoundingClientRect();
  return {x:(e.clientX-r.left)*canvas.width/r.width,y:(e.clientY-r.top)*canvas.height/r.height};
}
function hit(x,y){
  for(let i=items.length-1;i>=0;i--){
   const it=items[i],w=it.baseW*it.scale,h=it.baseH*it.scale;
   const dx=x-it.x,dy=y-it.y,c=Math.cos(-it.rotation),s=Math.sin(-it.rotation);
   const lx=dx*c-dy*s,ly=dx*s+dy*c;
   if(lx>=-w/2&&lx<=w/2&&ly>=-h/2&&ly<=h/2)return it;
  }
  return null;
}
function select(it){
  selected=it;updateControls();draw(true);
}
function normalizeImage(img){
  // New images initially fit comfortably inside the 2000x2800 field.
  const maxW=canvas.width*.72,maxH=canvas.height*.55;
  const fit=Math.min(1,maxW/img.naturalWidth,maxH/img.naturalHeight);
  return {baseW:img.naturalWidth*fit,baseH:img.naturalHeight*fit};
}
async function addFiles(list){
  const available=MAX-items.length;
  const chosen=[...list].filter(f=>f.type==='image/jpeg'||f.type==='image/png').slice(0,available);
  for(const file of chosen){
   const url=URL.createObjectURL(file);
   try{
    const img=new Image();
    await new Promise((ok,no)=>{img.onload=ok;img.onerror=no;img.src=url});
    const b=normalizeImage(img);
    const n=items.length;
    const offset=(n%8)*28;
    const it={img,baseW:b.baseW,baseH:b.baseH,scale:1,rotation:0,x:canvas.width/2+offset,y:canvas.height/2+offset};
    items.push(it);selected=it;
   }catch(e){}
   finally{URL.revokeObjectURL(url)}
  }
  updateControls();draw(true);makePng();
}
function reorder(mode){
  const i=selectedIndex();if(i<0)return;
  if(mode==='back'&&i>0){[items[i-1],items[i]]=[items[i],items[i-1]]}
  if(mode==='front'&&i<items.length-1){[items[i+1],items[i]]=[items[i],items[i+1]]}
  if(mode==='bottom'&&i>0){items.splice(i,1);items.unshift(selected)}
  if(mode==='top'&&i<items.length-1){items.splice(i,1);items.push(selected)}
  updateControls();draw(true);schedulePng();
}

sizeChoice.onchange=()=>setCanvasSize(sizeChoice.value);
upload.onclick=()=>files.click();
files.onchange=async e=>{await addFiles(e.target.files);e.target.value=''};
scale.oninput=()=>{
  if(!selected)return;
  selected.scale=Number(scale.value)/100;
  scaleValue.textContent=scale.value+'%';
  draw(true);schedulePng();
};
rotate.oninput=()=>{
  if(!selected)return;
  selected.rotation=Number(rotate.value)*Math.PI/180;
  rotateValue.textContent=rotate.value+'°';
  draw(true);schedulePng();
};
function rotateBy(deg){
  if(!selected)return;
  let d=Math.round(selected.rotation*180/Math.PI)+deg;
  while(d>180)d-=360;
  while(d<-180)d+=360;
  selected.rotation=d*Math.PI/180;
  rotate.value=d;rotateValue.textContent=d+'°';
  draw(true);schedulePng();
}
rotateLeft.onclick=()=>rotateBy(-15);
rotateRight.onclick=()=>rotateBy(15);
del.onclick=()=>{
  const i=selectedIndex();if(i<0)return;
  items.splice(i,1);selected=items[Math.min(i,items.length-1)]||null;
  updateControls();draw(true);makePng();
};
clear.onclick=()=>{
  items.length=0;selected=null;updateControls();draw(true);result.hidden=true;png.removeAttribute('src');
};
back.onclick=()=>reorder('back');
front.onclick=()=>reorder('front');
bottom.onclick=()=>reorder('bottom');
top.onclick=()=>reorder('top');

canvas.addEventListener('pointerdown',e=>{
  if(e.button!==0)return;
  const p=point(e),it=hit(p.x,p.y);
  select(it);
  if(!it)return;
  drag={id:e.pointerId,it,dx:p.x-it.x,dy:p.y-it.y};
  stage.classList.add('dragging');
  canvas.setPointerCapture(e.pointerId);
});
canvas.addEventListener('pointermove',e=>{
  if(!drag||drag.id!==e.pointerId)return;
  const p=point(e);
  drag.it.x=p.x-drag.dx;drag.it.y=p.y-drag.dy;
  draw(true);schedulePng();
});
function endDrag(e){
  if(!drag||drag.id!==e.pointerId)return;
  drag=null;stage.classList.remove('dragging');
  if(canvas.hasPointerCapture(e.pointerId))canvas.releasePointerCapture(e.pointerId);
  makePng();
}
canvas.addEventListener('pointerup',endDrag);
canvas.addEventListener('pointercancel',endDrag);
canvas.addEventListener('lostpointercapture',e=>{drag=null;stage.classList.remove('dragging')});

// Mouse wheel over a selected image also changes its scale.
canvas.addEventListener('wheel',e=>{
  const p=point(e),it=hit(p.x,p.y);
  if(!it)return;
  e.preventDefault();select(it);
  const next=Math.max(5,Math.min(400,Math.round(it.scale*100)+(e.deltaY<0?5:-5)));
  it.scale=next/100;scale.value=next;scaleValue.textContent=next+'%';
  draw(true);schedulePng();
},{passive:false});

updateControls();draw(true);
}
makeFreeEditor();

$$('.tab').forEach(tab=>tab.onclick=()=>{
$$('.tab').forEach(t=>{const on=t===tab;t.classList.toggle('active',on);t.setAttribute('aria-selected',on)});
$$('.panel').forEach(p=>p.hidden=p.id!==`panel-${tab.dataset.tab}`);
});
})();
</script>
</body>
</html>[/html]

0

8

[html]<iframe
    src="https://renegatjo-commits.github.io/collage/"
    width="100%"
    height="1600"
    frameborder="0"
    scrolling="yes">
</iframe>[/html]

0


Вы здесь » safe space design » Гостевой фолиант » тест примерки


Рейтинг форумов | Создать форум бесплатно