• 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-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/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/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/temp-001.js ./demo/temp-shape-scale-investigation.js ./demo/tensorflow-001.js ./demo/tensorflow-002.js ./demo/utilities.js
  • §

    Demo Canvas 009

    Pattern styles; Entity web link anchors; Dynamic accessibility

  • §

    Run code

    import * as scrawl from '../source/scrawl.js';
    
    import { reportSpeed, killArtefactAndAnchor, killStyle } from './utilities.js';
  • §

    Google Analytics

    We load GA code in the normal way through markup in the dom-006.html file (lines 11-21), and get a handle on the ga object here

    const ga = window[window['GoogleAnalyticsObject'] || 'ga'];
    
    let myTracker;
  • §

    Create a new tracker to handle tween and ticker action/progress, and set some attributes on it.

    • TS errors due to not importing/using GA types into the demo
    /** @ts-expect-error */
    ga('create', 'UA-000000-0', 'auto', 'demoCanvasTracker');
  • §

    We can then incorporate the tracker’s functionality in our various hook functions defined further down in this script

    /** @ts-expect-error */
    ga(function() {
    
        const ga = window[window['GoogleAnalyticsObject'] || 'ga'];
    
    /** @ts-expect-error */
        myTracker = ga.getByName('demoCanvasTracker');
        myTracker.set('transport', 'beacon');
        myTracker.set('campaignKeyword', 'Scrawl-canvas demo');
  • §

    Uncomment the next line to suppress tracker packets (so they don’t show up in the console)

        myTracker.set('sendHitTask', null);
    });
  • §

    Scene setup

    Get a handle to the Canvas wrapper

    const canvas = scrawl.findCanvas('mycanvas');
  • §

    Namespacing boilerplate

    const namespace = canvas.name;
    const name = (n) => `${namespace}-${n}`;
  • §

    Get images from DOM

    scrawl.importDomImage('.mypatterns');
  • §

    Create Pattern styles using imported images

    scrawl.makePattern({
    
        name: name('brick-pattern'),
        asset: 'brick',
    
    }).clone({
    
        name: name('leaves-pattern'),
        asset: 'leaves',
    
    });
  • §

    Create Pattern styles dynamically

    scrawl.makePattern({
    
        name: name('water-pattern'),
        imageSource: 'img/water.png',
    
    }).clone({
    
        name: name('marble-pattern'),
        imageSource: 'img/marble.png',
    
    });
  • §

    Create a canvas-based Cell pattern

    canvas.buildCell({
    
        name: name('cell-pattern'),
        width: 50,
        height: 50,
        backgroundColor: 'lightblue',
        shown: false,
    });
  • §

    Create a Block entity to display in the new Cell pattern

    scrawl.makeBlock({
    
        name: name('cell-pattern-block'),
        group: name('cell-pattern'),
    
        width: 40,
        height: 40,
    
        startX: 'center',
        startY: 'center',
    
        handleX: 'center',
        handleY: 'center',
    
        method: 'fill',
    
        fillStyle: name('water-pattern'),
    
        delta: {
            roll: -0.3
        },
    });
  • §

    Main canvas display - create Block entitys which will use the patterns defined above. Note that TS errors are due to:

    • Not importing/using GA types into the demo
    • TS not recognising that this refers to BlockInstance - it claims that get, set, clickAnchor etc functions don’t exist on BlockFactoryInputs (which is true), but we’re defining hook functions here. It can probably be fixed in the d.ts file, but I don’t know how to do that …
    scrawl.makeBlock({
    
        name: name('water-in-leaves'),
        group: canvas.get('baseGroup'),
    
        width: '40%',
        height: '40%',
    
        startX: '25%',
        startY: '25%',
    
        handleX: 'center',
        handleY: 'center',
    
        lineWidth: 20,
        lineJoin: 'round',
    
        method: 'fillThenDraw',
    
        fillStyle: name('cell-pattern'),
        strokeStyle: name('leaves-pattern'),
  • §

    To be aware: adding shadows to entitys using patterns for their fill and/or stroke styles can lead to a serious decrease in frame rate

        shadowOffsetX: 5,
        shadowOffsetY: 5,
        shadowBlur: 3,
        shadowColor: 'black',
  • §

    Include an anchor (href) link which can be tied to user interaction (in this case, mouse clicks on the canvas element) through events defined further down in this script.

  • §

    ALWAYS include a description! We will be using the anchor’s description string later as part of our asistive technology solution for this demo.

        anchor: {
            name: 'wikipedia-water-link',
            href: 'https://en.wikipedia.org/wiki/Water',
            description: 'Link to the Wikipedia article on water (opens in new tab)',
  • §

    The clickAction attribute captures both Scrawl-canvas trigger clicks and also non-mouse ‘clicks’ on the anchor element hidden at the top of the web page, for example using the keyboard (tab, return) or other assistive technology.

  • §

    This function gets invoked by an event listener added to the <a> link element in the DOM. this refers to the element, not the SC anchor wrapper or the Block entity object. The myTracker object is not recognised by the link element (the object’s scope is local to this module), so instead we fire the ga analytics object directly as that lives in the global space - see line 19 above.

            clickAction: function () {
    
    /** @ts-expect-error */
                ga('demoCanvasTracker.send', 'event', 'Outbound link', 'click', this.href);
            },
        },
  • §

    Accessibility functionality to be used by event functions defined below in response to user activity - this time moving the mouse cursor across the <canvas> element. Note that ‘this’ refers to the entity object, meaning the functions can be safely cloned into other entitys.

    /** @this {import('../source/scrawl.js').BlockInstance} */
        onEnter: function () {
  • §

    Update the block entity’s visual display

            this.set({
                lineWidth: 30,
            });
  • §

    This is where we update the accessibility information tied to the canvas element. We’re using the anchor attribute object’s description value to supply details of what actions will happen when the user clicks on the canvas while the mouse is over the block entity.

            canvas.set({
                title: `${this.get('name')} tile`,
                label: this.get('anchorDescription'),
            });
  • §

    Track the action in Google Analytics

    /** @ts-expect-error */
            myTracker.send('event', 'Canvas Entity', 'hover start', `${this.name} ${this.type}`);
        },
    
    /** @this {import('../source/scrawl.js').BlockInstance} */
        onLeave: function () {
  • §

    Reset the block entity’s visual display

            this.set({
                lineWidth: 20,
            });
  • §

    Reset the accessibility information tied to the canvas element.

            canvas.set({
                title: '',
                label: `${canvas.name} canvas element`,
            });
  • §

    Track the action in Google Analytics

    /** @ts-expect-error */
            myTracker.send('event', 'Canvas Entity', 'hover end', `${this.name} ${this.type}`);
        },
  • §

    Used by the Scrawl-canvas click event, below.

    • This hit report will only be generated from user interaction on the canvas element, thus will supply different numbers to the anchor’s clickAction function above - a useful way to help calculate the volume of users bypassing the canvas and opening the Wikipedia page using the keyboard or assistive technology
    /** @this {import('../source/scrawl.js').BlockInstance} */
        onUp: function () {
  • §

    Track the action in Google Analytics

    /** @ts-expect-error */
            myTracker.send('event', 'Canvas Entity Link', 'click', `${this.name} ${this.type} ${this.get('anchorHref')}`);
  • §

    Trigger the click event on the anchor element we added to the DOM

            this.clickAnchor();
        },
  • §

    Used by the Scrawl-canvas contextmenu event, below.

    • When the user right-clicks on a block, we want them to see the context menu for a link element, not an image element
    • Sadly I don’t know a good way to achieve this - creating a contextmenu Pointer event and dispatching it on the anchor’s element associated with the block doesn’t work for me
    • Instead, the following functionality clones the anchor’s element, styles and positions it, then adds it to the web page where user can (again) right-click on it to display the context menu
    • We then destroy the cloned element after five seconds
    • (If anyone has a better solution for this functionality, please create a PR!)
        onOtherInteraction: function (e) {
    
            e.preventDefault();
    
    /** @ts-expect-error */
            const el = this.anchor.domElement;
            const clone = el.cloneNode(true);
            const style = clone.style;
    
            clone.id = `${el.id}_clone`;
            style.position = 'fixed';
            style.left = `${e.clientX}px`;
            style.top = `${e.clientY}px`;
            style.transform = 'translate(-50%, -50%)';
            style.backgroundColor = 'rgb(200 200 200)';
            style.padding = '4px';
            style.border = '1px solid black';
            style.borderRadius = '4px';
    
            document.body.append(clone);
            setTimeout(() => clone.remove(), 5000);
        },
    
    }).clone({
    
        name: name('leaves-in-brick'),
    
        startX: '75%',
    
        fillStyle: name('leaves-pattern'),
        strokeStyle: name('brick-pattern'),
    
        anchor: {
            name: 'wikipedia-leaf-link',
            href: 'https://en.wikipedia.org/wiki/Leaf',
            description: 'Link to the Wikipedia article on leaves (opens in new tab)',
        },
    
    }).clone({
    
        name: name('brick-in-marble'),
    
        startY: '75%',
    
        fillStyle: name('brick-pattern'),
        strokeStyle: name('marble-pattern'),
    
        anchor: {
            name: 'wikipedia-brick-link',
            href: 'https://en.wikipedia.org/wiki/Brick',
            description: 'Link to the Wikipedia article on bricks (opens in new tab)',
        },
    
    }).clone({
    
        name: name('marble-in-water'),
    
        startX: '25%',
    
        fillStyle: name('marble-pattern'),
        strokeStyle: name('water-pattern'),
    
        anchor: {
            name: 'wikipedia-marble-link',
            href: 'https://en.wikipedia.org/wiki/Marble',
            description: 'Link to the Wikipedia article on marble (opens in new tab)',
        },
    });
  • §

    Demonstrate zoned actions on a canvas element as a result of user interaction

    • Available cascadeEventAction arguments are: ‘enter’, ‘leave’, ‘down’, or ‘up’
    • Also, the ‘move’ argument will trigger enter and leave actions on the entitys, as appropriate to each

    In this case, moving the mouse cursor over a block entity will increase its line width, as specified in the onEnter and onLeave functions in the block factories above.

    Additionally, it will update the <canvas> element’s title attribute (for tool tips) and its ARIA label value (for accessibility)

    The cascadeEventAction function returns an Array of name Strings for the entitys at the current mouse cursor coordinates

    const interactionResults = [];
    const interactions = function () {
    
        interactionResults.length = 0;
        if (canvas.here.active) interactionResults.push(...canvas.cascadeEventAction('move'));
    };
    scrawl.addListener('move', interactions, canvas.domElement);
  • §

    To capture other user interaction with the <a> DOM elements which, while being visually hidden, are still accessible - for instance when a user keyboard-tabs through the web page

    In this case, clicking on one of the tiles will open a related Wikipedia page in a new browser tab - as defined in the onUp function in the block factories above

    const mylinks = function () {
    
        if (canvas.here.active) canvas.cascadeEventAction('up');
    };
    scrawl.addNativeListener('click', mylinks, canvas.domElement);
  • §

    Handle a right-click contextmenu user interaction on a block

    • Note that the otherInteraction string was introduced with SC v8.14.0
    • An entity can have a maximum of one other interaction
    • The cascadeEventAction function can include the (mouse or pointer) event object as its second argument
    const myLinksContextMenu = function (e) {
    
        if (canvas.here.active) canvas.cascadeEventAction('otherInteraction', e);
    };
    scrawl.addNativeListener('contextmenu', (e) => myLinksContextMenu(e), canvas.domElement);
  • §

    Scene animation

    Function to display frames-per-second data, and other information relevant to the demo

    const report = reportSpeed('#reportmessage', function () {
        return `Hits: ${interactionResults}`;
    });
  • §

    Create the Display cycle animation

    scrawl.makeRender({
    
        name: name('animation'),
        target: canvas,
        afterShow: report,
    });
  • §

    Development and testing

    console.log(scrawl.library);
    
    console.log('Performing tests ...');
  • §

    We use the canvas and myTracker variables in our blocks’ onEnter, onLeave and onUp functions. While this works fine for the blocks created in the scope of this module file’s code, it will fail when we kill and resurrect a block - in the resurrected block the canvas and myTracker variables will be ‘undefined’. So we need to reset the block’s ‘on…’ functions (in this module file’s code) after the block has resurrected

    killArtefactAndAnchor(scrawl, canvas, name('brick-in-marble'), 'wikipedia-brick-link', 2000, () => {
    
        scrawl.findArtefact(name('brick-in-marble')).set({
    
    /** @this {import('../source/scrawl.js').BlockInstance} */
            onEnter: function () {
                this.set({ lineWidth: 30 });
                canvas.set({
                    title: `${this.name} tile`,
                    label: this.get('anchorDescription'),
                });
    /** @ts-expect-error */
                myTracker.send('event', 'Canvas Entity', 'hover start', `${this.name} ${this.type}`);
            },
    /** @this {import('../source/scrawl.js').BlockInstance} */
            onLeave: function () {
                this.set({ lineWidth: 20 });
                canvas.set({
                    title: '',
                    label: `${canvas.name} canvas element`,
                });
    /** @ts-expect-error */
                myTracker.send('event', 'Canvas Entity', 'hover end', `${this.name} ${this.type}`);
            },
    /** @this {import('../source/scrawl.js').BlockInstance} */
            onUp: function () {
    /** @ts-expect-error */
                myTracker.send('event', 'Canvas Entity Link', 'click', `${this.name} ${this.type} ${this.anchor.href}`);
                this.clickAnchor();
            },
            onOtherInteraction: function (e) {
                e.preventDefault();
    /** @ts-expect-error */
                const el = this.anchor.domElement;
                const clone = el.cloneNode(true);
                const style = clone.style;
                clone.id = `${el.id}_clone`;
                style.position = 'fixed';
                style.left = `${e.clientX}px`;
                style.top = `${e.clientY}px`;
                style.transform = 'translate(-50%, -50%)';
                style.backgroundColor = 'rgb(255 200 180)';
                style.padding = '4px';
                style.border = '1px solid black';
                style.borderRadius = '4px';
                document.body.append(clone);
                setTimeout(() => clone.remove(), 5000);
            },
        });
    });
    
    killStyle(scrawl, canvas, name('marble-pattern'), 3000, () => {
  • §

    Reset entitys, whose fill/strokeStyles will have been set to default values when the Pattern died

        scrawl.findEntity(name('brick-in-marble')).set({
            strokeStyle: name('marble-pattern'),
        });
    
        scrawl.findEntity(name('marble-in-water')).set({
            fillStyle: name('marble-pattern'),
        });
    });