• Jump To … +
    ./demo/canvas-001.js ./demo/canvas-002.js ./demo/canvas-003.js ./demo/canvas-004.js ./demo/canvas-005.js ./demo/canvas-006.js ./demo/canvas-007.js ./demo/canvas-008.js ./demo/canvas-009.js ./demo/canvas-010.js ./demo/canvas-011.js ./demo/canvas-012.js ./demo/canvas-013.js ./demo/canvas-014.js ./demo/canvas-015.js ./demo/canvas-016.js ./demo/canvas-017.js ./demo/canvas-018.js ./demo/canvas-019.js ./demo/canvas-020.js ./demo/canvas-021.js ./demo/canvas-022.js ./demo/canvas-023.js ./demo/canvas-024.js ./demo/canvas-025.js ./demo/canvas-026.js ./demo/canvas-027.js ./demo/canvas-028.js ./demo/canvas-029.js ./demo/canvas-030.js ./demo/canvas-031.js ./demo/canvas-032.js ./demo/canvas-033.js ./demo/canvas-034.js ./demo/canvas-035.js ./demo/canvas-036.js ./demo/canvas-037.js ./demo/canvas-038.js ./demo/canvas-039.js ./demo/canvas-040.js ./demo/canvas-041.js ./demo/canvas-042.js ./demo/canvas-043.js ./demo/canvas-044.js ./demo/canvas-045.js ./demo/canvas-046.js ./demo/canvas-047.js ./demo/canvas-048.js ./demo/canvas-049.js ./demo/canvas-050.js ./demo/canvas-051.js ./demo/canvas-052.js ./demo/canvas-053.js ./demo/canvas-054.js ./demo/canvas-055.js ./demo/canvas-056.js ./demo/canvas-057.js ./demo/canvas-058.js ./demo/canvas-059.js ./demo/canvas-060.js ./demo/canvas-061.js ./demo/canvas-062.js ./demo/canvas-063.js ./demo/canvas-064.js ./demo/canvas-065.js ./demo/canvas-066.js ./demo/canvas-067.js ./demo/canvas-068.js ./demo/canvas-069.js ./demo/canvas-070.js ./demo/canvas-071.js ./demo/canvas-072.js ./demo/canvas-073.js ./demo/canvas-201.js ./demo/canvas-202.js ./demo/canvas-203.js ./demo/canvas-204.js ./demo/canvas-205.js ./demo/canvas-206.js ./demo/canvas-207.js ./demo/canvas-208.js ./demo/canvas-209.js ./demo/canvas-210.js ./demo/canvas-211.js ./demo/canvas-212.js ./demo/delaunator-001.js ./demo/delaunator-002.js ./demo/dom-001.js ./demo/dom-002.js ./demo/dom-003.js ./demo/dom-004.js ./demo/dom-005.js ./demo/dom-006.js ./demo/dom-007.js ./demo/dom-008.js ./demo/dom-009.js ./demo/dom-010.js ./demo/dom-011.js ./demo/dom-012.js ./demo/dom-013.js ./demo/dom-015.js ./demo/dom-016.js ./demo/dom-017.js ./demo/dom-018.js ./demo/dom-019.js ./demo/dom-020.js ./demo/dom-021.js ./demo/filters-001.js ./demo/filters-002.js ./demo/filters-003.js ./demo/filters-004.js ./demo/filters-005.js ./demo/filters-006.js ./demo/filters-007.js ./demo/filters-008.js ./demo/filters-009.js ./demo/filters-010.js ./demo/filters-011.js ./demo/filters-012.js ./demo/filters-013.js ./demo/filters-014.js ./demo/filters-015.js ./demo/filters-016.js ./demo/filters-017.js ./demo/filters-018.js ./demo/filters-019.js ./demo/filters-020.js ./demo/filters-021.js ./demo/filters-022.js ./demo/filters-023.js ./demo/filters-024.js ./demo/filters-025.js ./demo/filters-026.js ./demo/filters-027.js ./demo/filters-028.js ./demo/filters-029.js ./demo/filters-030.js ./demo/filters-031.js ./demo/filters-032.js ./demo/filters-033.js ./demo/filters-034.js ./demo/filters-035.js ./demo/filters-036.js ./demo/filters-037.js ./demo/filters-101.js ./demo/filters-102.js ./demo/filters-103.js ./demo/filters-104.js ./demo/filters-105.js ./demo/filters-501.js ./demo/filters-502.js ./demo/filters-503.js ./demo/filters-504.js ./demo/filters-505.js ./demo/js/mediapipe/tasks-vision/vision-bundle.js ./demo/js/mediapipe/tasks-vision/wasm/vision_wasm_internal.js ./demo/js/mediapipe/tasks-vision/wasm/vision_wasm_nosimd_internal.js ./demo/mediapipe-001.js ./demo/mediapipe-002.js ./demo/mediapipe-003.js ./demo/modules-001.js ./demo/modules-002.js ./demo/modules-003.js ./demo/modules-004.js ./demo/modules-005.js ./demo/modules-006.js ./demo/modules/canvas-minimap.js ./demo/modules/canvas-scene-editor.js ./demo/modules/demo-m006-c1.js ./demo/modules/demo-m006-c2.js ./demo/modules/demo-m006-c3.js ./demo/modules/demo-m006-c4.js ./demo/modules/demo-m006-utils.js ./demo/modules/dom-entity-editor.js ./demo/modules/entity-copy-paste.js ./demo/modules/entity-manipulation-gui.js ./demo/modules/entity-navigation.js ./demo/modules/entity-ring-builder.js ./demo/modules/london-crime-lines.js ./demo/modules/london-crime-stacked-bars.js ./demo/modules/lottie-loader.js ./demo/modules/simple-chart-frame.js ./demo/modules/simple-graph-lines.js ./demo/modules/simple-graph-stacked-bars.js ./demo/modules/wikipedia-views-spiral-chart.js ./demo/packets-001.js ./demo/packets-002.js ./demo/particles-001.js ./demo/particles-002.js ./demo/particles-003.js ./demo/particles-004.js ./demo/particles-005.js ./demo/particles-006.js ./demo/particles-007.js ./demo/particles-008.js ./demo/particles-009.js ./demo/particles-010.js ./demo/particles-011.js ./demo/particles-012.js ./demo/particles-013.js ./demo/particles-014.js ./demo/particles-015.js ./demo/particles-016.js ./demo/particles-017.js ./demo/snippets-001.js ./demo/snippets-002.js ./demo/snippets-003.js ./demo/snippets-004.js ./demo/snippets-005.js ./demo/snippets-006.js ./demo/snippets/animated-highlight-gradient-text-snippet.js ./demo/snippets/animated-hover-gradient-snippet.js ./demo/snippets/animated-word-gradient-snippet.js ./demo/snippets/before-after-slider-infographic.js ./demo/snippets/bubbles-text-snippet.js ./demo/snippets/green-box-snippet.js ./demo/snippets/jazzy-button-snippet.js ./demo/snippets/page-performance-snippet-test.js ./demo/snippets/page-performance-snippet.js ./demo/snippets/pan-image-snippet.js ./demo/snippets/placeholder-effect-snippet.js ./demo/snippets/ripple-effect-snippet.js ./demo/snippets/risograph-text-gradient-snippet.js ./demo/snippets/spotlight-text-snippet-test.js ./demo/snippets/spotlight-text-snippet.js ./demo/snippets/swirling-stripes-text-snippet.js ./demo/snippets/word-highlighter-snippet.js ./demo/snippets/worley-text-gradient-snippet.js ./demo/temp-001.js ./demo/temp-shape-scale-investigation.js ./demo/tensorflow-001.js ./demo/tensorflow-002.js ./demo/utilities.js
  • §

    Demo Snippets 006

    Editable header text colorizer and animation effect snippets

    Related files:

    • Editable header text colorizer and animation effect snippets
    • Text snippet helper

    ‘Animated bubbles effect used to color text’ snippet

    Purpose: Displays text with an animation of rising bubbles over the text fill

    • This snippet supports dark-mode alternative colors
    • This snippet supports high contrast alternative colors
    • This snippet supports an animation effect which can be disabled in an accessible manner

    Function input:

    • the DOM element - generally a block or inline-block element.

    Customisation: The snippet can be customised using the following --data-??? CSS custom properties:

    • --data-text-color - any CSS color string (default: #c213bc')
    • --data-dark-text-color - any CSS color string (default: #fcdeef)
    • --data-outline-color - any CSS color string (default: #f59dcf)
    • --data-dark-outline-color - any CSS color string (default: #f59dcf)
    • --data-outline-width - (unit % of font size) percentage of the font size for text outline width (default: 0.05)
    • --data-bubble-color - any CSS color string (default: #fcdeef)
    • --data-dark-bubble-color - any CSS color string (default: #ed5fb0)
    • --data-bubble-outline-color - any CSS color string (default: #c213bc)
    • --data-dark-bubble-outline-color - any CSS color string (default: #c213bc)
    • --data-bubble-density - the number of bubbles to generate (default: 50)
    • --data-contrast-color - any CSS color string, used when user has set prefers-contrast: more (default: black)
    • --data-dark-contrast-color - any CSS color string, used when user has set prefers-contrast: more (default: white)

    Function output: a Javascript object will be returned, containing the following attributes

    {
        element     // the Scrawl-canvas wrapper for the DOM element supplied to the function
        canvas      // the Scrawl-canvas wrapper for the snippet's canvas
        animation   // the Scrawl-canvas animation object
        demolish    // remove the snippet from the Scrawl-canvas library
    }
    
    Usage example:
    import * as scrawl from 'path/to/scrawl-canvas/library';
    
    import mySnippet from './relative/or/absolute/path/to/this/file.js';
    let myElements = document.querySelectorAll('.some-class');
    myElements.forEach(el => mySnippet(scrawl, el));
    
  • §

    Effects on the element:

    • Imports the element’s background color, and sets the element background to transparent
    • Imports the element’s text node text, and sets the text color to transparent
    export default function (scrawl, el) {
  • §

    Boilerplate - namespacing

        const namespace = el.id;
        const name = (val) => `${namespace}-${val}`;
  • §

    Only progress if the supplied element has an id attribute

        if (namespace) {
  • §

    Create the snippet for this DOM element

            const snippet = scrawl.makeSnippet({
                domElement: el,
            });
  • §

    Only proceed if the snippet is successfully generated

            if (snippet) {
  • §

    Unpack the snippet into the parts we’ll be using

                const canvas = snippet.canvas,
                    animation = snippet.animation,
                    demolishAction = snippet.demolish,
                    compStyles = snippet.element.elementComputedStyles;
  • §

    Boilerplate - text processing

                const addTextNode = () => {
                    const shy = document.createTextNode('!');
                    el.appendChild(shy);
                };
    
                const processText = t => {
                    t = t.replace(/<canvas.*<\/canvas>/gi, '');
                    t = t.replace(/<button.*<\/button>/gi, '');
                    if (!t.length) {
                        addTextNode();
                        t = '!';
                    }
                    return t;
                }
  • §

    Boilerplate - demolish/kill functionality

                const additionalDemolishActions = [];
    
                snippet.demolish = () => {
                    additionalDemolishActions.forEach(f => f());
                    scrawl.purge(namespace);
                    demolishAction();
                };
  • §

    This makes the canvas element’s base cell the default group for everything we create

                canvas.setAsCurrentCanvas();
  • §

    Boilerplate - fix for text alignment

                const getJustifyLine = (val) => {
    
                    if (val === 'justify') return 'space-between';
                    if (val === 'justify-all') return 'space-around';
                    if (val === 'match-parent') return 'start';
                    return val;
                };
  • §

    Boilerplate - fix for lineSpacing/lineHeight

                const getLineSpacing = () => parseFloat(compStyles.lineHeight) / parseFloat(compStyles.fontSize);
  • §

    Initialize and collect developer-supplied data

    • We also set the defaults here for missing colors/values
                const userData = {
    
                    direction: compStyles.direction || 'ltr',
                    fontStretch: compStyles.fontStretch || 'normal',
                    letterSpacing: compStyles.letterSpacing || '0px',
                    wordSpacing: compStyles.wordSpacing || '0px',
                    fontVariantCaps: compStyles.fontVariantCaps || 'normal',
                    lineAdjustment: compStyles.getPropertyValue('--data-line-adjustment') || '0',
                    justifyLine: getJustifyLine(compStyles.textAlign),
    
                    elBackgroundColor: compStyles.backgroundColor || 'transparent',
    
                    textColor: compStyles.getPropertyValue('--data-text-color') || '#c213bc',
                    darkTextColor: compStyles.getPropertyValue('--data-dark-text-color') || '#fcdeef',
                    outlineColor: compStyles.getPropertyValue('--data-outline-color') || '#f59dcf',
                    darkOutlineColor: compStyles.getPropertyValue('--data-dark-outline-color') || '#f59dcf',
                    bubbleColor: compStyles.getPropertyValue('--data-bubble-color') || '#fcdeef',
                    darkBubbleColor: compStyles.getPropertyValue('--data-dark-bubble-color') || '#ed5fb0',
                    bubbleOutlineColor: compStyles.getPropertyValue('--data-bubble-outline-color') || '#c213bc',
                    darkBubbleOutlineColor: compStyles.getPropertyValue('--data-dark-bubble-outline-color') || '#c213bc',
                    contrastColor: compStyles.getPropertyValue('--data-contrast-color') || 'black',
                    darkContrastColor: compStyles.getPropertyValue('--data-dark-contrast-color') || 'white',
                    outlineWidth: compStyles.getPropertyValue('--data-outline-width') || '0.05',
                    bubbleDensity: compStyles.getPropertyValue('--data-bubble-density') || '50',
                };
    
                const getLineWidth = () => parseFloat(userData.outlineWidth) * parseFloat(compStyles.fontSize);
  • §

    Build the animated bubbles text effect

                const fizz = scrawl.makeWheel({
                    name: name('bubble-template'),
                    order: 1,
                    startY: '100%',
                    handleX: 'center',
                    handleY: 'center',
                    fillStyle: userData.bubbleColor,
                    strokeStyle: userData.bubbleOutlineColor,
                    lineWidth: getLineWidth() * 0.7,
                    globalCompositeOperation: 'source-atop',
                    method: 'none',
                });
    
                const bubblesGroup = scrawl.makeGroup({
                    name: name('bubbles-group'),
                });
    
                const tweens = [];
    
                for (let i = 0; i < userData.bubbleDensity; i++) {
    
                    const bubble = fizz.clone({
                        name: name(`bubble-${i}`),
                        radius: Math.round((Math.random() * (parseFloat(compStyles.fontSize) / 2)) + 4),
                        startX: `${Math.random() * 100}%`,
                        noCanvasEngineUpdates: true,
                        sharedState: true,
                        method: 'fillThenDraw',
                    });
    
                    bubblesGroup.addArtefacts(bubble);
    
                    const myRandom = Math.random();
    
                    tweens.push(scrawl.makeTween({
                        name: name(`bubble-${i}`),
                        targets: bubble,
                        duration: Math.round((myRandom * 3000) + 5000),
                        cycles: 0,
                        definitions: [{
                            attribute: 'startY',
                            start: '100%',
                            end: '0%',
                        }, {
                            attribute: 'scale',
                            start: 0.3,
                            end: Math.round((1 - myRandom) * 0.9) + 0.6,
                        }],
                    }).run());
                }
    
                const template = scrawl.makeBlock({
                    name: name('template'),
                    dimensions: ['100%', '100%'],
                    visibility: false,
                });
    
                const label = scrawl.makeEnhancedLabel({
                    name: name('content'),
                    layoutTemplate: name('template'),
    
                    text: processText(el.innerHTML),
                    fontString: compStyles.font,
    
                    textHandleY: 'alphabetic',
                    visibility: false,
                    cacheOutput: false,
    
                    direction: userData.direction,
                    fontStretch: userData.fontStretch,
                    letterSpacing: userData.letterSpacing,
                    wordSpacing: userData.wordSpacing,
                    fontVariantCaps: userData.fontVariantCaps,
                    lineSpacing: getLineSpacing(),
                    justifyLine: userData.justifyLine,
    
                    fillStyle: userData.textColor,
                    strokeStyle: userData.outlineColor,
                    lineWidth: getLineWidth(),
    
                    method: 'fillAndDraw',
                });
  • §

    Boilerplate - font adjustments

                let meta;
    
                const getLineAdjustment = () => {
    
                    const size = parseFloat(compStyles.fontSize);
                    const ratio = size / 100;
                    return ratio * (meta.alphabeticBaseline + meta.verticalOffset + parseFloat(userData.lineAdjustment));
                };
    
                const updateOnFontLoad = () => {
    
                    const font = compStyles.fontFamily,
                        check = scrawl.checkFontIsLoaded(font);
    
                    if (check) {
    
                        el.style.backgroundColor = 'transparent';
                        el.style.color = 'transparent';
    
                        meta = scrawl.getFontMetadata(font);
    
                        const displacement = getLineAdjustment();
    
                        template.set({
                            startY: displacement,
                            handleY: displacement,
                            fillStyle: userData.elBackgroundColor,
                            visibility: true,
                        });
    
                        label.set({ visibility: true });
    
                        bubblesGroup.set({ visibility: true });
    
                        animation.updateHook('commence');
                    }
                };
    
                animation.updateHook('commence', updateOnFontLoad);
  • §

    Boilerplate user interaction - resizing the browser window

                let resizeFlag = true,
                    lastResize = Date.now();
    
                const resizeChoke = 200;
    
                const setResizeFlag = () => {
    
                    resizeFlag = true;
    
                    const now = Date.now();
  • §

    Canvases don’t animate when outside of the browser viewport (to save CPU, battery, etc)

    • This check forces those canvases to update once to adapt to the new viewport size
    • Doing this should prevent unexpected horizontal scrollbars appearing on the page
    • Should also minimize flashes of badly sized content when canvas scrolls into view
                    if (!animation.isRunning() && now > lastResize + resizeChoke) {
    
                        resizeAction();
                        animation.updateOnce();
                        lastResize = now;
                    }
                };
    
                const resizeAction = () => {
    
                    if (resizeFlag) {
    
                        resizeFlag = false;
    
                        label.set({ fontString: compStyles.font });
    
                        if (meta) {
    
                            const displacement = getLineAdjustment();
    
                            template.set({
                                startY: displacement,
                                handleY: displacement,
                            });
    
                            label.set({
                                letterSpacing: compStyles.letterSpacing,
                                wordSpacing: compStyles.wordSpacing,
                                lineWidth: getLineWidth(),
                            });
    
                            bubblesGroup.setArtefacts({
                                lineWidth: getLineWidth() * 0.7,
                            });
                        }
                    }
                };
    
                animation.updateHook('afterShow', resizeAction);
    
                additionalDemolishActions.push(
                    scrawl.addNativeListener('resize', setResizeFlag, window),
                );
  • §

    Boilerplate user interaction - editing the text

                if (el.getAttribute('contenteditable')) {
    
                    const updateText = () => {
                        label.set({ text: processText(el.innerHTML) });
                    }
                    const focusText = () => {
                        el.style.color = 'rgb(0 0 0 / 0.4)';
                    }
                    const blurText = () => {
                        el.style.color = 'transparent';
                    }
    
                    additionalDemolishActions.push(
                        scrawl.addNativeListener('input', updateText, el),
                        scrawl.addNativeListener('focus', focusText, el),
                        scrawl.addNativeListener('blur', blurText, el),
                    );
                }
  • §

    Boilerplate - animation control

                if ('static' === compStyles.position) el.style.position = 'relative';
    
                const control = document.createElement('button');
    
                control.style.position = 'absolute';
                control.style.fontSize = '12px';
                control.style.display = 'block';
                control.style.top = '0';
                control.style.right = '0';
                control.textContent = 'Halt';
                control.setAttribute('contenteditable', 'false');
    
                el.appendChild(control);
    
                let isAnimated = true;
    
                const stopStartAction = () => {
    
                    isAnimated = !isAnimated;
    
                    if (isAnimated) tweens.forEach(t => t.resume());
                    else tweens.forEach(t => t.halt());
    
                    control.textContent = isAnimated ? 'Halt' : 'Play';
                };
    
                additionalDemolishActions.push(
                    scrawl.addNativeListener('click', stopStartAction, control)
                );
  • §

    Accessibility

                const reduceMotionAction = () => {
    
                    if (isAnimated) {
    
                        isAnimated = false;
    
                        tweens.forEach(t => t.halt());
    
                        control.textContent = 'Play';
                    }
                };
    
                const noPreferenceMotionAction = () => {
    
                    if (!isAnimated) {
    
                        isAnimated = true;
    
                        tweens.forEach(t => t.resume());
    
                        control.textContent = 'Halt';
                    }
                };
    
                const colorSchemeLightAction = () => {
    
                    fizz.set({
    
                        fillStyle: userData.bubbleColor,
                        strokeStyle: userData.bubbleOutlineColor,
                    });
    
                    label.set({
    
                        fillStyle: userData.textColor,
                        strokeStyle: userData.outlineColor,
                    });
                };
    
                const colorSchemeDarkAction = () => {
    
                    fizz.set({
    
                        fillStyle: userData.darkBubbleColor,
                        strokeStyle: userData.darkBubbleOutlineColor,
                    });
    
                    label.set({
    
                        fillStyle: userData.darkTextColor,
                        strokeStyle: userData.darkOutlineColor,
                    });
                };
    
                const moreContrastAction = () => {
    
                    const prefersDark = canvas.here.prefersDarkColorScheme;
    
                    const color = (prefersDark) ? userData.darkContrastColor : userData.contrastColor;
    
                    label.set({
                        fillStyle: color,
                        method: 'fill',
                    });
    
                    bubblesGroup.set({ visibility: false });
                };
    
                const otherContrastAction = () => {
    
                    const prefersDark = canvas.here.prefersDarkColorScheme;
    
                    const color = (prefersDark) ? userData.darkTextColor : userData.textColor;
    
                    label.set({
                        fillStyle: color,
                        method: 'fillAndDraw',
                    });
    
                    bubblesGroup.set({ visibility: true });
                };
    
                canvas.set({
                    colorSchemeLightAction,
                    colorSchemeDarkAction,
                    moreContrastAction,
                    otherContrastAction,
                    reduceMotionAction,
                    noPreferenceMotionAction,
                });
  • §

    Render once, to get everything in place

                animation.updateOnce();
  • §

    Return the snippet, so coders can access the snippet’s parts

    • In case they need to tweak the output to meet the web page’s specific requirements
                return snippet;
            }
        }
        return null;
    }