• Jump To … +
    ./demo/modules/canvas-minimap.js ./demo/modules/canvas-scene-editor.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-graphic.js ./demo/modules/london-crime-lines.js ./demo/modules/london-crime-stacked-bars.js ./demo/modules/lottie-loader.js ./demo/modules/simple-chart-frame-tests.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/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/text-snippet-helper.js ./demo/snippets/word-highlighter-snippet.js ./demo/snippets/worley-text-gradient-snippet.js
  • §

    Demo Snippets 005

    Create a responsive, interactive and accessible before/after slider infographic

    Related files:

    • Slider infographic snippet - main module
  • §

    ‘Before-after slider infographic’ snippet

    Purpose: Turn a pair of images, alongside associated data, into a fully responsive, interactive and accessible before-after slider infographic

    Note that this is a highly complex snippet, which relies on recieving an element to process in a very specific format containing a number of required child elements. All elements are expected to have various specified data- attributes which are used to build the infographic.

    Function input:

    <div 
      id="infographic-unique name"
      class="some-class-name"
      data-label="Infographic title - required for accessibility"
      data-description="Infographic description - required for accessibility">
      
      <div 
        id="left-panel-unique-name"
          data-date="date of image"
        data-frame="left">
    
        <img 
          id="left-panel-image-unique-name"
          alt="Alternative text for image - required for accessibility" 
          src="path/to/image/url" />
    
        <!--
          Each &lt;p> element needs an id value together with some data- attributes
    
            - data-type="pin" - required (no default value)
              - for proof of concept we're only handling spot pins, not area pins
    
            - data-position - required (no default value)
              - a string of two percentage values, separated by a comma 
              - these are the 'x%, y%' coords where the pin will appear on image
    
            - data-fill - pin fill color - default is "red"
            - data-stroke - pin stroke color - default is "yellow"
            - data-labelwidth - percentage label width value - default is "20%"
            - data-labelbackground - default is "rgba(0,0,0,0.2)"
    
            - data-labelposition - default is "below"
              - where we want the text to appear relative to the pin's location 
              - acceptable values: "left", "right", "above", "below"
    
            - data-shared - default is not included
              - if included, we create the pin on both panels
    
          The text in the <p> element is the text that will be used for the pin's 
          label. Note that the snippet has been set up to interpret and display 
          the following inline markup: 
            - <b></b>, <strong></strong>, <i></i>, <em></em>
            - <span class="sc-red"></span>, etc
        -->
    
        <p 
          id="pin-unique-id" 
          data-type="pin" 
          data-position="59%, 21%" 
          data-fill="blue",
          data-stroke="yellow",
          data-labelwidth="30%"
          data-labelposition="left"
          data-labelbackground="azure"
          data-shared>Text <span class="sc-red">appears</span> when user <strong>hovers</strong> over pin</p>
    
      </div>
    
      <div 
        id="right-panel-unique-name"
          data-date="date of image"
        data-frame="right">
    
        <img 
          id="right-panel-image-unique-name"
          alt="Alternative text for image - required for accessibility" 
          src="path/to/image/url" />
    
        <p>...Pin definitions</p>
    
      </div>
    
      <!--
        If we also want to include any clickable links in the infographic, 
        we can include them in a &lt;nav> element
          - data-position - required (no default value)
            - a string of two percentage values, separated by a comma 
            - these are the 'x%, y%' coords where the link will appear over image
    
          - data-justify - one of: "left", "right", "center" (default)
          - data-width - defaults to "20%"
          - data-background - defaults to "rgba(0,0,0,0.2)" 
      -->
    
      <nav>
        <a 
          id="link-unique-id" 
          href="https://absolute/link/url" 
          data-position="1%, 95%" 
          data-width="45%"
          data-justify="left">Text to include in the link</a>
    
        <a 
          id="another-link-unique-id" 
          href="relative/link/url" 
          data-position="99%, 95%" 
          data-width="45%"
          data-background="darkblue"
          data-justify="right">Different text for different link</a>
      </nav>
    
    </div>
    
  • §
    Usage example:
    import slider from './relative/or/absolute/path/to/this/file.js';
    
    let myElements = document.querySelectorAll('.some-class-name');
    
    myElements.forEach(el => slider(el));
    

    Note that this snippet has a profound effect on the element it processes, moving images inside a canvas element and deleting most of the child elements (to prevent copy repetition for screen readers)

    Function output:

    {
        element           // wrapper
        canvas            // wrapper
        animation         // object
        demolish          // function
    }
    

    Snippet code

    Import the Scrawl-canvas object

    import * as scrawl from '../../source/scrawl.js'
  • §

    Internal function to scrape data from the supplied element

    const getPanelData = function (el, store, canvas) {
    
        if (!el) return false;
    
        const img = el.querySelector('img');
    
        store.element = el;
        store.name = el.id;
        store.date = el.dataset.date;
        store.imageElement = img;
        store.pinElements = el.querySelectorAll('p');
        store.pins = [];
    
        store.pinElements.forEach(p => {
    
            store.pins.push({
                name: p.id,
                groupname: store.name,
                type: p.dataset.type,
                position: p.dataset.position,
                fill: p.dataset.fill || 'red',
                stroke: p.dataset.stroke || 'yellow',
                labelposition: p.dataset.labelposition,
                labelwidth: p.dataset.labelwidth || '25%',
                labelbackground: p.dataset.labelbackground || 'rgba(0,0,0,0.2)',
                labeltext: p.innerHTML,
                shared: (p.dataset.shared != null) ? true : false,
            });
        });
    
        canvas.domElement.appendChild(img);
    
        return store;
    };
  • §

    Internal function to scrape data from the supplied element

    const getNavigationData = function (el, store, canvas) {
    
        if (!el) return false;
    
        store.element = el;
        store.links = [];
    
        const linkElements = el.querySelectorAll('a');
    
        linkElements.forEach(a => {
            store.links.push({
                name: a.id,
                href: a.href,
                width: a.dataset.width || '20%',
                position: a.dataset.position,
                background: a.dataset.background || 'rgba(0,0,0,0.2)',
                justify: a.dataset.justify || 'center',
                text: a.innerHTML,
            });
        });
    
        return store;
    };
  • §

    The pin factory takes all the data about pins that we scraped from the element and builds a set of interactive Scrawl-canvas entitys for each pin

    const pinFactory = function (items, canvas, pinTextGroup, pinTextBackgroundGroup, colors) {
    
        let { name, groupname, position, fill, stroke, labeltext, labelposition, labelwidth, labelbackground, shared, suppressAccessibleText } = items;
    
        const coords = position.split(',');
  • §

    TODO: We don’t want to display a label if its associated pin is not currently visible to the user

        const checkVisibility = function (entity) {
    
            return true;
        }
  • §

    Generate the visible pin

        const pin = scrawl.makeWheel({
    
            name: `${name}-pin`,
            group: `${groupname}-cell`,
    
            start: coords,
            handle: ['center', 'center'],
    
            radius: 10,
            fillStyle: fill,
            strokeStyle: stroke,
            lineWidth: 2,
            method: 'fillThenDraw',
    
        });
  • §

    The hit zone for the pin is assigned to the canvas element’s base cell, to make the user interaction (eg collision detection with the mouse cursor) code as simple as posssible

        pin.clone({
    
            name: `${name}-hitzone`,
            group: canvas.base.name,
            radius: 25,
            method: 'none',
    
            onEnter: function () {
    
                if (checkVisibility(pin)) {
  • §

    Increase the visible pin’s size

                    pin.set({ 
                        radius: 15,
                        lineWidth: 4,
                    });
  • §

    Display the pin’s associated label

                    if (pinText) {
    
                        pinText.set({ visibility: true });
                        pinBackground.set({ visibility: true });
                    }
                }
            },
    
            onLeave: function () {
    
                if (checkVisibility(pin)) {
  • §

    Decrease the visible pin’s size

                    pin.set({ 
                        radius: 10,
                        lineWidth: 2,
                    });
  • §

    Hide the pin’s associated label

                    if (pinText) {
    
                        pinText.set({ visibility: false });
                        pinBackground.set({ visibility: false });
                    }
                }
            },
        });
  • §

    Generate the label associated with each pin (assuming it’s been defined in the data)

        let pinText, pinBackground;
    
        if (labeltext) {
  • §

    Labels can be positioned above, below, or to the right or left of the pin.

            if (!labelposition) labelposition = 'below';
    
            let handle = ['center', '-20%'];
    
            if ('above' === labelposition) handle = ['center', '120%'];
            if ('left' === labelposition) handle = ['110%', 'center'];
            if ('right' === labelposition) handle = ['-10%', 'center'];
    
            pinText = scrawl.makePhrase({
    
                name: `${name}-label`,
                group: `${groupname}-cell`,
                order: 1,
    
                text: labeltext,
                justify: 'center',
                lineHeight: 1.15,
    
                width: labelwidth,
  • §

    @ts-expect-error

                handle,
    
                pivot: `${name}-pin`,
                lockTo: 'pivot',
    
                font: '28px Arial, sans-serif',
                fillStyle: colors.default,
    
                exposeText: (suppressAccessibleText) ? false : true,
    
                visibility: false,
            });
    
            pinTextGroup.addArtefacts(`${name}-label`);
    
            pinBackground = scrawl.makeBlock({
                name: `${name}-background`,
                group: `${groupname}-cell`,
                order: 0,
                fillStyle: labelbackground,
                width: 20,
                height: 20,
                handleY: 10,
    
                mimic: `${name}-label`,
                lockTo: 'mimic',
                useMimicDimensions: true,
                useMimicStart: true,
                useMimicHandle: true,
    
                addOwnDimensionsToMimic: true,
                addOwnHandleToMimic: true,
                visibility: false,
            });
    
            pinTextBackgroundGroup.addArtefacts(`${name}-background`);
        }
    };
  • §

    The link factory takes all the data about links that we scraped from the element and builds a set of interactive Scrawl-canvas entitys for each link

    const linkFactory = function (items, canvas, linkTextGroup, colors) {
    
        const {name, position, href, justify, text, width, background} = items;
    
        const start = position.split(',');
    
        scrawl.makePhrase({
    
            name: `${name}-link`,
            group: canvas.base.name,
            order: 1,
            text,
            start,
    
            width,
    
            handleX: justify,
            handleY: justify,
            justify,
    
            font: '16px Arial, sans-serif',
            lineHeight: 1,
            fillStyle: colors.link,
    
            underlinePosition: 0.8,
            underlineStyle: colors.linkunderline,
    
            onEnter: function () {
    
                canvas.set({
                    css: {
                        cursor: 'pointer',
                    }
                });
  • §

    @ts-expect-error

                this.set({
                    text: `§UNDERLINE§${this.text}`,
                });
            },
    
            onLeave: function () {
    
                canvas.set({
                    css: {
                        cursor: 'auto',
                    }
                });
  • §

    @ts-expect-error

                this.set({
                    text: this.text.replace('§UNDERLINE§', ''),
                });
            },
    
            onUp: function () {
  • §

    @ts-expect-error

                this.clickAnchor();
            },
  • §

    Suppress accessible text here because we’re also including an anchor, which will also add its description text to the canvas shadow DOM (as part of the navigation)

            exposeText: false,
    
            anchor: {
                name,
                href,
                description: text,
    
                focusAction: true,
                blurAction: true,
            },
    
        });
    
        linkTextGroup.addArtefacts(`${name}-link`);
    
        scrawl.makeBlock({
            name: `${name}-background`,
            group: canvas.base.name,
            order: 0,
            fillStyle: background,
            width: 20,
            height: 10,
            handleY: 10,
    
            mimic: `${name}-link`,
            lockTo: 'mimic',
            useMimicDimensions: true,
            useMimicStart: true,
            useMimicHandle: true,
    
            addOwnDimensionsToMimic: true,
            addOwnHandleToMimic: true,
        })
    };
  • §
    The exported function
    export default function (el) {
  • §

    Apply the snippet to the DOM element

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

    0. Convenience handle variables; basic followup housekeeping

            let canvas = snippet.canvas,
                animation = snippet.animation,
                wrapper = snippet.element,
                element = wrapper.domElement,
                name = wrapper.name;
    
            canvas.set({
                backgroundColor: 'white',
            }).render();
    
            wrapper.set({
                canvasOnTop: true,
            });
  • §

    1. Get all information required from the wrapper’s panel <div> child elements

            const leftPanel = getPanelData(element.querySelector('[data-frame="left"]'), {}, canvas);
    
            const rightPanel = getPanelData(element.querySelector('[data-frame="right"]'), {}, canvas);
  • §

    Snippet will fail if we don’t have both panels to build

            if (leftPanel && rightPanel) {
  • §

    2. Get all information about links from the wrapper’s <nav> child element

                const navItems = getNavigationData(element.querySelector(':scope > nav'), {}, canvas);
  • §

    3. Get all information required from the wrapper element

                const aria_label = element.dataset.label,
                    aria_description = element.dataset.description;
  • §

    4. Clean up DOM - we don’t need to keep elements, text or links that we’ll be recreating in the <canvas> element

                leftPanel.element.remove();
                rightPanel.element.remove();
    
                if (navItems && navItems.element) navItems.element.remove();
    
                delete element.dataset.label;
                delete element.dataset.description;
  • §

    5. Import image assets

                scrawl.importDomImage(`#${name} img`);
  • §

    6. Create some Groups for handling User Interaction functionality

                const pinTextGroup = scrawl.makeGroup({
                    name: `${name}-pin-texts-group`,
                });
    
                const pinTextBackgroundGroup = scrawl.makeGroup({
                    name: `${name}-pin-texts-background-group`,
                });
    
                const linkTextGroup = scrawl.makeGroup({
                    name: `${name}-link-texts-group`,
                });
  • §

    7. Setup the canvas element

                canvas.set({
  • §

    Accessibility first!

                    label: aria_label,
                    description: aria_description,
                    includeInTabNavigation: true,
  • §

    Responsive text - we need to make sure text updates to an appropriate, readable size when the user changes the dimensions of their browser’s window, or alters the orientation of their device.

                    breakToSmallest: 200000,
                    breakToSmaller: 400000,
                    breakToLarger: 600000,
                    breakToLargest: 800000,
    
                    actionSmallestArea: function () {
                        pinTextGroup.setArtefacts({ size: '28px'});
                        linkTextGroup.setArtefacts({ size: '14px'});
                    },
                    actionSmallerArea: function () {
                        pinTextGroup.setArtefacts({ size: '26px'});
                        linkTextGroup.setArtefacts({ size: '17px'});
                    },
                    actionRegularArea: function () {
                        pinTextGroup.setArtefacts({ size: '24px'});
                        linkTextGroup.setArtefacts({ size: '20px'});
                    },
                    actionLargerArea: function () {
                        pinTextGroup.setArtefacts({ size: '22px'});
                        linkTextGroup.setArtefacts({ size: '23px'});
                    },
                    actionLargestArea: function () {
                        pinTextGroup.setArtefacts({ size: '20px'});
                        linkTextGroup.setArtefacts({ size: '26px'});
                    },
    
                }).setBase({
                    compileOrder: 1,
                });
  • §

    Infographic keyboard navigation

                let displayAllLabels = false;
    
                const toggleLabels = () => {
    
                    displayAllLabels = !displayAllLabels;
    
                    pinTextGroup.setArtefacts({
                        visibility: displayAllLabels,
                    });
    
                    pinTextBackgroundGroup.setArtefacts({
                        visibility: displayAllLabels,
                    });
                }
    
                const moveBar = (pos, width) => {
    
                    dragBar.set({
                        startX: pos,
                    });
    
                    rightImage.set({
                        copyStartX: pos,
                        copyWidth: width,
                        width,
                    });
                };
    
                const showMore = (moveLeft = true) => {
    
                    const canvasWidth = canvas.get('width');
                    const currentPos = dragBar.get('position');
                    const [x, y] = currentPos;
    
                    const dir = (moveLeft) ? -1 : 1;
    
                    let distance = ((x * 100) / canvasWidth) + dir;
                    if (distance < 0) distance = 0;
                    else if (distance > 100) distance = 100;
    
                    const pos = `${distance.toFixed(2)}%`;
                    const width = `${(100 - distance).toFixed(2)}%`;
    
                    return [pos, width];
                };
    
                const showAllBefore = () => moveBar('100%', '0%');
                const showAllAfter = () => moveBar('0%', '100%');
                const showMoreBefore = () => moveBar(...showMore(false));
                const showMoreAfter = () => moveBar(...showMore(true));
    
                const canvasKeys = (e) => {
    
                    const { keyCode, shiftKey, isComposing } = e;
  • §

    Ignore when user is composing a glyph

                    if (isComposing || 229 === keyCode) return;
  • §

    Tab, Enter/Return, Esc

                    if (9 === keyCode || 13 === keyCode || 27 === keyCode) {
                        canvas.domElement.blur();
                        return;
                    }
    
                    e.preventDefault();
  • §

    Left/right arrow keys (with and without shift)

                    if (shiftKey) {
                        if (39 === keyCode) showAllBefore();
                        else if (37 === keyCode) showAllAfter();
                    }
                    else {
                        if (39 === keyCode) showMoreBefore();
                        else if (37 === keyCode) showMoreAfter();
                        else if (32 === keyCode) toggleLabels();
                    }
                }
                scrawl.addNativeListener('keydown', canvasKeys, canvas.domElement);
  • §

    Grab colors from CSS

                const colors = {},
                    cssColors = getComputedStyle(document.documentElement);
    
                colors.default = cssColors.getPropertyValue('--sc-default');
                colors.shadow = cssColors.getPropertyValue('--sc-shadow');
                colors.link = cssColors.getPropertyValue('--sc-link');
                colors.linkshadow = cssColors.getPropertyValue('--sc-linkshadow');
                colors.linkunderline = cssColors.getPropertyValue('--sc-linkunderline');
                colors.red = cssColors.getPropertyValue('--sc-red');
                colors.green = cssColors.getPropertyValue('--sc-green');
                colors.blue = cssColors.getPropertyValue('--sc-blue');
                colors.black = cssColors.getPropertyValue('--sc-black');
                colors.white = cssColors.getPropertyValue('--sc-white');
                colors.gray = cssColors.getPropertyValue('--sc-gray');
  • §

    Add additional section classes to the library

                const sect = scrawl.library.sectionClasses;
    
                sect['span class="sc-red"'] = { fill: colors.red };
                sect['span class="sc-blue"'] = { fill: colors.blue };
                sect['span class="sc-green"'] = { fill: colors.green };
                sect['span class="sc-white"'] = { fill: colors.white };
                sect['span class="sc-black"'] = { fill: colors.black };
                sect['span class="sc-gray"'] = { fill: colors.gray };
                sect['/span'] = { fill: 'default' };
  • §

    8. Build the left-hand panel

                const leftPanelName = leftPanel.name;
    
                canvas.buildCell({
                    name: `${leftPanelName}-cell`,
  • §

    Does height need to be hardcoded? We should really pro-rata it in line with container/canvas dimensions?

                    dimensions: [1000, 500],
                    shown: false,
                });
    
                scrawl.makePicture({
                    name: `${leftPanelName}-image`,
                    group: `${leftPanelName}-cell`,
                    asset: leftPanel.imageElement.id,
                    dimensions: ['100%', '100%'],
                    copyDimensions: ['100%', '100%'],
                });
    
                scrawl.makePhrase({
                    name: `${leftPanelName}-image-date`,
                    group: `${leftPanelName}-cell`,
                    order: 1,
                    start: ['1%', '1%'],
                    handle: ['left', 'top'],
                    text: leftPanel.date,
                    font: '20px Arial, sans-serif',
                    lineHeight: 0.7,
                    fillStyle: colors.white,
                });
    
                scrawl.makeBlock({
                    name: `${leftPanelName}-image-date-background`,
                    group: `${leftPanelName}-cell`,
                    order: 0,
                    fillStyle: colors.black,
                    width: 20,
                    height: 20,
                    handle: [10, 10],
                    mimic: `${leftPanelName}-image-date`,
                    lockTo: 'mimic',
                    useMimicDimensions: true,
                    useMimicStart: true,
                    useMimicHandle: true,
                    addOwnDimensionsToMimic: true,
                    addOwnHandleToMimic: true,
                });
    
                scrawl.makePicture({
                    name: `${leftPanelName}-image-display`,
                    group: canvas.base.name,
                    asset: `${leftPanelName}-cell`,
                    dimensions: ['100%', '100%'],
                    copyDimensions: ['100%', '100%'],
                });
  • §

    9. Build the right-hand panel

                const rightPanelName = rightPanel.name;
    
                canvas.buildCell({
                    name: `${rightPanelName}-cell`,
  • §

    Again, does height need to be hardcoded?

                    dimensions: [1000, 500],
                    shown: false,
                });
    
                scrawl.makePicture({
                    name: `${rightPanelName}-image`,
                    group: `${rightPanelName}-cell`,
                    asset: rightPanel.imageElement.id,
                    dimensions: ['100%', '100%'],
                    copyDimensions: ['100%', '100%'],
                });
    
                scrawl.makePhrase({
                    name: `${rightPanelName}-image-date`,
                    group: `${rightPanelName}-cell`,
                    order: 1,
                    start: ['99%', '1%'],
                    handle: ['right', 'top'],
                    text: rightPanel.date,
                    font: '20px Arial, sans-serif',
                    lineHeight: 0.7,
                    fillStyle: colors.white,
                });
    
                scrawl.makeBlock({
                    name: `${rightPanelName}-image-date-background`,
                    group: `${rightPanelName}-cell`,
                    order: 0,
                    fillStyle: colors.black,
                    width: 20,
                    height: 20,
                    handle: [10, 10],
                    mimic: `${rightPanelName}-image-date`,
                    lockTo: 'mimic',
                    useMimicDimensions: true,
                    useMimicStart: true,
                    useMimicHandle: true,
                    addOwnDimensionsToMimic: true,
                    addOwnHandleToMimic: true,
                });
  • §

    We only need to manipulate the attributes of the right hand panel to execute the slider functionality

                const rightImage = scrawl.makePicture({
                    name: `${rightPanelName}-image-display`,
                    group: canvas.base.name,
                    asset: `${rightPanelName}-cell`,
                    dimensions: ['50%', '100%'],
                    copyDimensions: ['50%', '100%'],
                    copyStartX: '50%',
                    start: ['right', 'top'],
                    handle: ['right', 'top'],
                });
  • §

    10. Generate pins and their associated labels

                leftPanel.pins.forEach(p => {
    
                    if (p.shared) {
    
                        const n = p.name;
    
                        p.name = `${n}-left`
                        p.groupname = leftPanelName;
                        
                        pinFactory(p, canvas, pinTextGroup, pinTextBackgroundGroup, colors);
    
                        p.name = `${n}-right`
                        p.groupname = rightPanelName;
                        p.suppressAccessibleText = true;
                    }
                    pinFactory(p, canvas, pinTextGroup, pinTextBackgroundGroup, colors);
                });
    
                rightPanel.pins.forEach(p => {
    
                    if (p.shared) {
    
                        const n = p.name;
    
                        p.name = `${n}-left`
                        p.groupname = leftPanelName;
                        
                        pinFactory(p, canvas, pinTextGroup, pinTextBackgroundGroup, colors);
    
                        p.name = `${n}-right`
                        p.groupname = rightPanelName;
                        p.suppressAccessibleText = true;
                    }
                    pinFactory(p, canvas, pinTextGroup, pinTextBackgroundGroup, colors);
                });
  • §

    11. Build the drag bar entitys

                const dragBar = scrawl.makeBlock({
    
                    name: `${name}-drag-bar`,
                    group: canvas.base.name,
                    start: ['center', 'center'],
                    handle: ['center', 'center'],
                    dimensions: [80, '110%'],
    
                    method: 'none',
    
                    onEnter: function () {
    
                        canvas.set({
                            css: {
                                cursor: 'col-resize',
                            }
                        });
                    },
    
                    onLeave: function () {
    
                        canvas.set({
                            css: {
                                cursor: 'auto',
                            }
                        });
                    },
                });
    
                scrawl.makeBlock({
    
                    name: `${name}-drag-line`,
                    group: canvas.base.name,
                    dimensions: [0, '110%'],
                    handle: ['center', 'center'],
    
                    pivot: `${name}-drag-bar`,
                    lockTo: 'pivot',
    
                    strokeStyle: 'white',
                    lineWidth: 4,
                    method: 'draw',
                });
    
                scrawl.makeOval({
    
                    name: `${name}-drag-bar-button`,
                    group: canvas.base.name,
                    handle: ['center', 'center'],
                    radiusX: 40,
                    radiusY: 25,
    
                    pivot:   `${name}-drag-bar`,
                    lockTo: 'pivot',
    
                    fillStyle: 'yellow',
                    strokeStyle: 'white',
                    lineWidth: 2,
                    globalAlpha: 0.6,
                    method: 'fillThenDraw',
                });
  • §

    12. Build the drag bar functionality

    • KNOWN BUG - the drag bar not draggable on first user mousedown, but is draggable afterwards
                scrawl.makeGroup({
                    name: `${name}-drag-group`,
                }).addArtefacts(`${name}-drag-bar`,);
    
                let isDragging = false;
    
                const dragAction = function () {
    
                    if (isDragging) {
    
                        const canvasWidth = canvas.get('width');
                        const currentPos = dragBar.get('position');
                        const [x, y] = currentPos;
    
                        const distance = (x * 100) / canvasWidth;
                        const pos = `${distance.toFixed(2)}%`;
                        const width = `${(100 - distance).toFixed(2)}%`;
    
                        rightImage.set({
                            copyStartX: pos,
                            copyWidth: width,
                            width,
                        });
                    }
                };
    
                scrawl.makeDragZone({
    
                    zone: canvas,
                    collisionGroup: `${name}-drag-group`,
                    endOn: ['up', 'leave'],
    
                    updateOnStart: () => {
  • §

    @ts-expect-error

                        dragBar.isBeingDragged = false;
                        dragBar.set({
                            lockXTo: 'mouse',
                        });
    
                        isDragging = true;
                    },
    
                    updateOnEnd: () => {
    
                        let [x, y] = dragBar.get('position'),
                            width = canvas.get('width');
    
                        dragBar.set({
                            startX: `${((x * 100) / width).toFixed(2)}%`,
                            startY: 'center',
                            lockXTo: 'start',
                        });
    
                        isDragging = false;
                    },
                    preventTouchDefaultWhenDragging: true,
                });
  • §

    13. Build the interactive links

                if (navItems && navItems.links) navItems.links.forEach(n => linkFactory(n, canvas, linkTextGroup, colors));
  • §

    14. Hook into event listeners

                scrawl.addListener('move', () => canvas.cascadeEventAction('move'), el);
                scrawl.addNativeListener('touchmove', () => canvas.cascadeEventAction('move'), el);
                scrawl.addNativeListener(['click', 'touchend'], () => canvas.cascadeEventAction('up'), el);
  • §

    15. Update the animation object to listen for drag bar activity

                animation.set({ commence: dragAction });
            }
        }
  • §

    We delay making the element’s contents visible - this is to prevent ugly flashes of original DOM content

        el.style.visibility = 'visible';
  • §

    Return the snippet output

        return snippet;
    };
    
    console.log(scrawl.library);