Нарисуйте звездное небо с помощью CSS Houdini.

JavaScript SVG CSS Canvas
Нарисуйте звездное небо с помощью CSS Houdini.

Если вы хотите спросить, какая технология CSS является самой захватывающей в 2018 году, CSS Houdini заслуживает этого, и вы даже можете снять ограничение 2018 года. На самом деле эта технология вышла в 2016 году, но официально она поддерживалась в Chrome 65, выпущенном в марте этого года.

CSS Houdini Что можно сделать?Документация Google для разработчиковПеречислены несколько демонстраций, давайте сначала посмотрим на эти демонстрации:

(1) Добавьте клетчатый фон в текстовую область (demo)

Используйте следующий код CSS:

textarea {
    background-image: paint(checkerboard);
}

(2) Добавьте ромбовидный фон в div (demo)

Используйте следующий CSS:

div {
    --top-width: 80;
    --top-height: 20;
    -webkit-mask-image: paint(demo);
}

(3) Щелкните анимацию рассеяния по кругу (demo)

Во всех трех примерах используется CSS Paint API в Houdini.

В первом примере, если мы используем традиционные свойства CSS, мы можем использовать градиенты для изменения цвета максимум, но мы не можем сделать такое изменение цвета от сетки к сетке, а во втором примере нет возможности рисовать напрямую с CSS в форме ромба. В это время вы можете подумать о методе SVG/Canvas.Характеристики SVG и Canvas — это векторные пути, которые могут рисовать различную векторную графику, а Canvas также может управлять произвольными пикселями, поэтому эти два метода также можно использовать. .

Но при совмещении Canvas и html будет немного коряво, как и во втором примере рисовать ромбовидную форму, с Canvas нужно использовать метод, аналогичный позиционированию BFC, настроить каваны в соответствующее положение, а также обратить внимание к z -index оверлейному отношению, но может быть проще использовать SVG, вы можете установить фоновое изображение как ромбовидное изображение svg, но вы не можете так же легко управлять некоторыми переменными, как Canavas, например, изменение цвета и толщины алмазной границы в любое время. Подождите.

В первом примере вы можете использовать только background-image + svg, чтобы добавить фоновую сетку в текстовую область, но вы не знаете, насколько велика текстовая область Сколько сеток svg нужно подготовить? Конечно, вы можете сказать, кто добавил бы такой фон в текстовую область. Но это всего лишь пример, другие сценарии могут столкнуться с подобными проблемами.

Третий пример нажимает на анимацию диффузии круга, которая также может абсолютно найти элемент холста в div, но мы сталкиваемся с другой проблемой: его нельзя легко использовать повторно, предполагая, что этот эффект диффузии круга также используется в других местах, тогда вам нужно везде напишите элемент холста и инициализируйте его.

Таким образом, традиционный метод имеет следующие проблемы:

(1) Необходимо настроить позиционирование и взаимосвязь z-index с другими элементами html и т. д.

(2) Фон окна редактирования и т. д. не может быть легко изменен, а управление переменными не может быть легко выполнено.

(3) Его нельзя легко использовать повторно

На самом деле, есть еще одна более важная проблема, это производительность.При использовании Cavans для отрисовки этого эффекта вам необходимо самостоятельно контролировать частоту кадров.Если вы не будете осторожны, вентилятор процессора компьютера может свистеть, особенно если вы можете' t понять сроки перерисовки, если элемент Если размер не меняется, его не нужно перерисовывать.Если элемент увеличен, его нужно перерисовать, или при наведении мышки его нужно перерисовать для анимации .

CSS Houdini обеспечивает хорошее решение проблемы рисования пользовательской графики и изображений, которые могутИспользуйте Canvas, чтобы нарисовать желаемую графику, а затем зарегистрируйте ее в системе CSS, вы можете использовать эту графику в свойствах CSS.. Возьмите рисунок звездного неба в качестве примера, чтобы проиллюстрировать процесс шаг за шагом.

1. Нарисуйте темное ночное небо

CSS Houdini может работать только в доменном имени localhost или в среде https, в противном случае соответствующий API не виден (не определен). Если нет среды https, вы можете установить пакет npm http-сервера, затем запустить его локально, получить доступ к localhost: 8080, создать новый index.html и написать:

<!DOCType>
<html>
<head>
    <meta charset="utf-8">
<style>
body {
    background-image: paint(starry-sky);
}
</style>    
</head>
<body>
<script>
    CSS.paintWorklet.addModule('starry-sky.js');
</script>
</body>
</html>

Зарегистрируйте CSS-графику звездного неба, вызвав CSS.paintWorklet.addModule в JS, а затем вы сможете использовать эту графику в CSS и прописать ее в таких свойствах, как background-image, border-image или mask-image. Как в коде выше:

body {
    background-image: paint(starry-sky);
}

При регистрации ворклета вам нужно дать ему независимый js, как рабочую среду ворклета, которая такая же, как веб-воркер без таких объектов, как окно/документ. Если вы не хотите писать и управлять слишком большим количеством js-файлов, вы можете использовать blob, который может хранить данные любого типа, включая JS-файлы.

Код Madry-Sky.js Требуется следующим образом:

class StarrySky {
    paint (ctx, paintSize, properties) {
        // 使用Canvas的API进行绘制
        ctx.fillRect(0, 0, paintSize.width, paintSize.height);
    }
}
// 注册这个属性
registerPaint('starry-sky', StarrySky);

Напишите класс для реализации интерфейса рисования, который будет передавать переменную контекста холста, размер текущего холста, то есть размер текущего элемента dom, и свойства свойства css текущего элемента dom.

В функции рисования вызовите функцию рисования холста fillRect для заполнения, а цвет заливки по умолчанию — черный. Посетите index.html, и вы увидите, что вся страница становится черной. Наш Hello World CSS Houdini Painter запущен и работает, да, это так просто.

Но следует подчеркнуть, что реализация браузера не добавляет Canvas к элементу dom, а затем скрывает его.Этот Paint Worket на самом деле напрямую влияет на процесс перерисовки текущего элемента dom, что эквивалентно добавлению к нему перерисовки. , продолжение которого будет ниже.

Если вы не хотите писать js самостоятельно, вы можете использовать blob следующим образом:

let blobURL = URL.createObjectURL( new Blob([ '(',
    function(){
        
        class StarrySky {
            paint (ctx, paintSize, properties) {
                ctx.fillRect(0, 0, paintSize.width, paintSize.height);
            }
        }
        registerPaint('starry-sky', StarrySky);

    }.toString(),
 
    ')()' ], { type: 'application/javascript' } ) 
);

CSS.paintWorklet.addModule(blobURL);

2. Рисуем звезды

Эффект звезды холста хорошо найти в Интернете, например, этотCodepen, код показан ниже:

paint (ctx, paintSize, poperties) {
    let xMax= paintSize.width;
    let yMax = paintSize.height;

    // 黑色夜空
    ctx.fillRect(0, 0, xMax, yMax);
    
    // 星星的数量
    let hmTimes = xMax + yMax;  
    for (let i = 0; i <= hmTimes; i++) {
        // 星星的xy坐标,随机
        let x = Math.floor((Math.random() * xMax) + 1); 
        let y = Math.floor((Math.random() * yMax) + 1); 
        // 星星的大小
        let size = Math.floor((Math.random() * 2) + 1); 
        // 星星的亮暗
        let opacityOne = Math.floor((Math.random() * 9) + 1); 
        let opacityTwo = Math.floor((Math.random() * 9) + 1); 
        let hue = Math.floor((Math.random() * 360) + 1); 
        ctx.fillStyle = `hsla(${hue}, 30%, 80%, .${opacityOne + opacityTwo})`; ctx.fillRect(x, y, size, size); } }

Эффект следующий:

Почему он использует fillRect для рисования звезд, разве звезды не должны быть круглыми? Потому что если вы используете дугу, производительность будет значительно снижена. Так как звезды относительно маленькие, используется этот метод.Конечно, его также можно изменить на дугу, потому что мы рисуем ее только один раз.

3. Контролируйте плотность звезд

Теперь создайте настраиваемый параметр для управления плотностью звезд, точно так же, как можно контролировать радиус границы. С помощью переменных CSS добавьте пользовательское свойство --star-density в body:

body {
    --star-density: 0.8;
    background-image: paint(starry-sky); 
}

Указанный коэффициент плотности изменяется от 0 до 1, а свойства получаются через параметр свойства функции рисования. Однако мы обнаружили, что пользовательские атрибуты body/html не могут быть получены и могут быть унаследованы дочерними элементами body, но не могут быть получены в теле, поэтому они изменены на отрисовку в body:before:

body:before {
    content: "";
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    --star-density: 0.5;
    background-image: paint(starry-sky); 
}

Затем добавьте статический метод в класс StarrySky:

class StarrySky {
    static get inputProperties() {
        return ['--star-density'];
    }
}

Сообщите нам, какие свойства CSS нам нужно получить, которые могут быть пользовательскими или обычными свойствами CSS. Затем вы можете получить значение свойства в свойствах метода рисования:

class StarrySky {
    paint (ctx, paintSize, properties) {
        // 获取自定义属性值
        let starDensity = +properties.get('--star-density').toString() || 1;
        // 最大只能为1
        starDensity > 1 && (starDensity = 1);
        // 星星的数量剩以这个系数
        let hmTimes = Math.round((xMax + yMax) * starDensity);
    }
}

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

3. Перекрасить

Потянув страницу, вы обнаружите, что позиции всех звезд изменились, это связано с тем, что срабатывает перерисовка.

Добавьте console.log в функцию рисования, и когда вы откроете страницу, вы увидите, что браузер постоянно выполняет функцию рисования. Поскольку это свойство CSS записано в body:befoer, оно заполняет тело, а изменение размера тела вызывает перерисовку. И если это написано в div с фиксированной шириной, вытягивание страницы не вызовет перерисовку, и наблюдается, что функция рисования не выполняется. Если вы измените какое-либо свойство CSS элемента div или body, это также вызовет перерисовку. Так что это очень удобно, и нам не нужно отслеживать изменения DOM, такие как изменение размера.

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

4. Запишите данные звезд

Вы можете добавить в класс SkyStarry переменную-член, чтобы сохранить всю информацию о звездах, включая положение и прозрачность. Оценивайте длину звезд при рисовании. Если она равна 0, инициализируйте ее, в противном случае используйте звезду, которая была инициализирована непосредственно в прошлый раз. , что гарантирует, что одни и те же звезды используются для каждой перекраски. Однако в процессе реальной работы обнаружилась проблема: он дважды инициализирует starry-sky.js, а также случайным образом переключается при рисовании, как показано на следующем рисунке:

Это приводит к тому, что данные двух звезд переключаются между собой в процессе перерисовки. Причина, вероятно, в том, что CSS Houdini на самом деле не хочет, чтобы вы сохраняли данные экземпляра, но, поскольку он разработан как класс, также имеет смысл использовать данные экземпляра класса. Одно решение, которое я придумал для этой проблемы, состоит в том, чтобы сделать случайную функцию управляемой.Пока семена рандомизации одинаковы, сгенерированные случайные серии будут одинаковыми, а семена рандомизации передаются переменными CSS. Таким образом, вы не можете использовать Math.random, вы можете реализовать рандом самостоятельно,Следующий кодпоказано:

    random () {
        let x = Math.sin(this.seed++) * 10000;
        return x - Math.floor(x);
    }

Пока инициализированные семена одинаковы, будут генерироваться одни и те же случайные серии. Семя похоже на плотность звезд, управляемую переменной CSS:

body:before {
    --starry-sky-seed: 1;
    --star-density: 0.5;
    background-image: paint(starry-sky);
}

Затем получите начальное значение через свойства в функции рисования:

paint (ctx, paintSize, properties) {
    if (!this.stars) {
        let starOpacity = +properties.get('--star-opacity').toString();
        // 得到随机化种子,可以不传,默认为0
        this.seed = +(properties.get('--starry-sky-seed').toString() || 0);
        this.addStars(paintSize.width, paintSize.height, starDensity);
    }
}

Добавьте звезды с помощью функции addStars, которая вызывает пользовательскую случайную функцию выше:

random () {
    let x = Math.sin(this.seed++) * 10000;
    return x - Math.floor(x);
}

addStars (xMax, yMax, starDensity = 1) {
    starDensity > 1 && (starDensity = 1); 
    // 星星的数量
    let hmTimes = Math.round((xMax + yMax) * starDensity);  
    this.stars = new Array(hmTimes);
    for (let i = 0; i < hmTimes; i++) {
        this.stars[i] = { 
            x: Math.floor((this.random() * xMax) + 1), 
            y: Math.floor((this.random() * yMax) + 1), 
            size: Math.floor((this.random() * 2) + 1), 
            // 星星的亮暗
            opacityOne: Math.floor((this.random() * 9) + 1), 
            opacityTwo: Math.floor((this.random() * 9) + 1), 
            hue: Math.floor((this.random() * 360) + 1)
        };  
    }
}

Этот код изменен с Math.random на this.random, чтобы гарантировать, что пока начальное число рандомизации одинаково, все сгенерированные данные будут одинаковыми. Это может решить проблему двойной инициализации данных, упомянутых выше, потому что начальные значения одинаковы, поэтому данные одинаковы дважды.

Но это как-то однообразно, при каждом обновлении страницы звезды фиксируются, ауры меньше. Это семя рандомизации можно оптимизировать, например, оно будет одинаковым в течение одного часа, а изменится после обновления страницы через час. Этого можно добиться с помощью следующего кода:

const ONE_HOUR = 36000 * 1000;
this.seed = +(properties.get('--starry-sky-seed').toString() || 0)
                    + Date.now() / ONE_HOUR >> 0;

Таким образом, звезды не будут меняться при открытии страницы.

Но когда я рос, звезд справа не было:

Так как первый холст был не таким большим, а данные звезд позже не обновлялись, то правая часть была пуста.

5. Увеличьте данные звезд обновления

Вы не можете обновить все данные звезд, иначе шаг 4 будет сделан зря. Исправить можно только то, что не по праву. Таким образом, вам нужно дважды записать размер холста.Если второй холст больше, добавьте звезды, иначе удалите звезды за границей.

Поэтому должна быть переменная для записи размера последнего холста:

class StarrySky {
    constructor () {
        // 初始化
        this.lastPaintSize = this.paintSize = {
            width: 0,
            height: 0
        };
        this.stars = [];
    }
}

Извлечение связанных операций в функцию, включая получение настроек из переменных CSS, постепенное обновление звездочек и т. д., может немного прояснить основную логику:

paint (ctx, paintSize, properties) {
    // 更新当前paintSize
    this.paintSize = paintSize;
    // 获取CSS变量设置,把密度、seed等存放到类的实例数据
    this.updateControl(properties);
    // 增量更新星星
    this.updateStars();
    // 黑色夜空
    for (let star of this.stars) {
        // 画星星,略
    }   
}

Инкрементное обновление звезд требует двух суждений: одно — нужно ли удалить некоторые звезды, а другое — нужно ли их добавлять в соответствии с изменениями холста:

updateStars () {
    // 如果当前的画布比上一次的要小,则删掉一些星星
    if (this.lastPaintSize.width > this.paintSize.width ||
            this.lastPaintSize.height > this.paintSize.height) {
        this.removeStars();
    }   
    // 如果当前画布变大了,则增加一些星星
    if (this.lastPaintSize.width < this.paintSize.width ||  
            this.lastPaintSize.height < this.paintSize.height) {
        this.addStars();
    }   
    this.lastPaintSize = this.paintSize;
}

Реализация удаления звезды removeStar очень проста, достаточно оценить, находятся ли координаты x, y в текущем холсте, и если да, то сохранить:

removeStars () {
    let stars = []
    for (let star of stars) {
        if (star.x <= this.paintSize.width &&  
                star.y <= this.paintSize.height) {
            stars.push(star);
        }   
    }   
    this.stars = stars;
}

Реализация добавления звезд тоже аналогична, судят, находятся ли координаты x, y в предыдущем холсте, и если да, то не добавлять:

addStars () {
    let xMax = this.paintSize.width,
        yMax = this.paintSize.height;
    // 星星的数量
    let hmTimes = Math.round((xMax + yMax) * this.starDensity); 
    for (let i = 0; i < hmTimes; i++) {
        let x = Math.floor((this.random() * xMax) + 1), 
            y = Math.floor((this.random() * yMax) + 1); 
        // 如果星星落在上一次的画布内,则跳过
        if (x < this.lastPaintSize.width && y < this.lastPaintSize.height) {
            continue;
        }   

        this.stars.push({
            x: x,
            y: y,
            size: Math.floor((this.random() * 2) + 1), 
            // 星星的亮暗
        }); 
    }   
}

Перетащите страницу, чтобы, когда время заставит RedRaw, RedRaw, когда обновление настроится настроить звезды.

6. Пусть сияют звезды

Анимировав прозрачность звезд, вы можете заставить звезды сиять. Если вы используете тег Cavans, вы можете использовать window.requestAnimationFrame для регистрации функции, а затем вычесть модуль начального времени из текущего времени, чтобы получить текущий коэффициент прозрачности по значению. Этот метод также можно использовать с Houdini, разница в том, что мы можем использовать динамически изменяющийся коэффициент прозрачности как переменную CSS или пользовательский атрибут текущего элемента, а затем использовать JS для динамического изменения пользовательского атрибута для запуска перерисовки. упоминается в разделе перерисовки.

Добавьте к элементу атрибут --star-opacity:

body:before {
    --star-opacity: 1;
    --star-density: 0.5;
    --starry-sky-seed: 1;
    background-image: paint(starry-sky);
}

В случае со звездами прозрачность каждой звезды умножается на этот коэффициент:

// 获取透明度系数
this.starOpacity = +properties.get('--star-opacity').toString();
for (let star of this.stars) {
    // 每个星星的透明度都乘以这个系数
    let opacity = +('.' + (star.opacityOne + star.opacityTwo)) * this.starOpacity;
    ctx.fillStyle = `hsla(${star.hue}, 30%, 80%, ${opacity})`;
    ctx.fillRect(star.x, star.y, star.size, star.size);
}

Затем динамически измените это свойство CSS в requestAnimationFrame:

let start = Date.now();
// before无法获取,所以需要改成正常元素
let node = document.querySelector('.starry-sky');
window.requestAnimationFrame(function changeOpacity () {
    let now = Date.now();
    // 每隔一1s,透明度从0.5变到1
    node.style.setProperty('--star-opacity', (now - start) % 1000 / 2 + 0.5);
    window.requestAnimationFrame(changeOpacity);
});

Таким образом, функция рисования может быть повторно запущена для повторного рендеринга, но этот эффект на самом деле проблематичен, потому что должен быть альтернативный эффект, то есть 0,5 меняется на 1, а затем изменяется с 1 на 0,5, вместо 0,5 каждый раз К 1. Это легко решить, моделируя альтернативу анимации CSS. Можно указать, что нечетные секунды станут больше, а четные секунды станут меньше. Это легко реализовать, и опущен.

Но на самом деле вам не нужно быть таким неприятным, потому что вы можете напрямую использовать анимацию для изменения свойств CSS, как показано в следующем коде:

body:before {
    --star-opacity: 1;
    --star-density: 0.5;
    --starry-sky-seed: 1;
    background-image: paint(starry-sky);
    animation: shine 1s linear alternate infinite;
}

@keyframes shine {
    from {
        --star-opacity: 1;
    }
    to {
        --star-opacity: 0.6;
    }
}

Это также может вызвать перерисовку, но мы обнаружили, что она вызывает перерисовку только в двух точках от и до, а промежуточный процесс перехода отсутствует. Можно предположить, что поскольку он считает, что значение атрибута --star-opacity является не числом, а строкой, между этими двумя ключевыми кадрами нет промежуточного эффекта перехода. Поэтому мы должны сообщить ему, что это целое число, а не строка. Типизированная объектная модель CSS (Typed CSSOM) предоставляет этот API.

Типизированная объектная модель CSSБольшая роль заключается в представлении всех единиц CSS соответствующим объектом, обеспечивающим такие операции, как сложение, вычитание, умножение и деление, например:

// 10 px
let length = CSS.px(10);
// 在循环里面改length的值,不用自己去拼字符串
div.attributeStyleMap.set('width', length.add(CSS.px(1)))

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

Он также предоставляет возможность регистрировать свойства пользовательского типа с помощью следующих API:

CSS.registerProperty({
    name: '--star-opacity',
    // 指明它是一个数字类型
    syntax: '<number>',
    inherits: false,
    initialValue: 1
});

После этой регистрации система CSS знает, что --star-opacity является числовым типом, и в анимации ключевого кадра будет эффект градиентного перехода.

Типовая объектная модель CSS официально поддерживается в Chrome 66, но API registerProperty по-прежнему не открыт. Вам нужно открыть chrome://flags, найти веб-платформу и перейти с отключенного на включенное для использования.

Это дает намНовые идеи для анимации., анимация CSS + режим Canvas, анимация CSS отвечает за изменение данных свойств и запуск перерисовки, а Canvas — за получение динамически изменяющихся данных для обновления представления. так что этоРежим управляемой данными анимации, который также является популярным способом создания анимации.

В нашем примере из-за того, что звезд слишком много, в 1с 60 кадров, в каждом кадре нужно рассчитать и отрисовать 1000 звезд, а коэффициент использования ЦП более 90%, поэтому с этой производительностью проблемы. Если вы используете тег Cavans, вы можете использовать технологию двойной буферизации, CSS Houdini, похоже, не имеет этой вещи. Но вы можете изменить способ мышления и сделать анимацию полной прозрачности вместо подсчета каждой звезды.

Как показано в следующем коде:

body {
    background-color: #000; 
}
body:before {
    background-image: paint(starry-sky);
    animation: shine 1s linear alternate infinite;
}

@keyframes shine {
    from {
        opacity: 1;
    }
    to {
        opacity: 0.6;
    }
}

Эффект от этого такой же, как и у каждой звезды по отдельности, а потребление процессора составляет около 12%, что все еще должно быть приемлемым.

Результаты, как показано ниже:

Если вы используете тег Canvas, вы можете установить свойство глобальной прозрачности globalAlpha, а с помощью CSS Houdini мы можем напрямую использовать непрозрачность.

Полная демонстрация:CSS Houdini Starry Sky, требует использования Chrome, так как в настоящее время поддерживается только Chrome.


В общем, Paint Worket CSS Houdini обеспечивает связующее звено между CSS и Canvas, позволяя нам рисовать желаемый эффект CSS с помощью Canvas и управлять им с помощью пользовательских свойств CSS, используя анимацию/переход JS или CSS. Изменение значения пользовательского свойства вызывает перерисовку. , что приводит к эффекту анимации, что также является идеей разработки, управляемой данными. А также обсудили некоторые проблемы, возникающие в процессе рисования этого звездного неба, и связанные с ними решения.

В этой статье представлены только Paint Worket и Typed CSSOM в CSS Houdini.Также в ней есть еще один Layout Worklet, который можно использовать для реализации гибкого макета или других пользовательских макетов.Преимущества: с одной стороны, когда появляется новый макет Когда вы можете использовать этот API для полифиллинга, вам не нужно беспокоиться о несовместимости нереализованных браузеров.С другой стороны, вы можете использовать свое воображение, чтобы реализовать желаемый макет, так что может быть сотня цветов в компоновка, а не только те, что даны W3C.

[Рекомендованная книга снова] Внесен в список эффективный внешний интерфейс, доступный на JD.com, Amazon, Taobao и т. д.

Renren Recruitment Расширенный интерфейс