/** @ts-expect-error */
import Delaunator from 'https://cdn.skypack.dev/delaunator@5.0.0';
/** @ts-expect-error */
import Delaunator from 'https://cdn.skypack.dev/delaunator@5.0.0';
The following functions are used to handle the Delaunay object
const edgesOfTriangle = (t) => [3 * t, 3 * t + 1, 3 * t + 2];
const pointsOfTriangle = (del, t) => {
const { triangles } = del;
return edgesOfTriangle(t).map(e => triangles[e]);
};
const triangleOfEdge = (e) => Math.floor(e / 3);
const triangleCenter = (pts, del, t) => {
const vertices = pointsOfTriangle(del, t).map(p => pts[p]);
return circumcenter(vertices[0], vertices[1], vertices[2]);
};
const circumcenter = (a, b, c) => {
if (a && b && c && a.length && b.length && c.length) {
const ad = a[0] * a[0] + a[1] * a[1],
bd = b[0] * b[0] + b[1] * b[1],
cd = c[0] * c[0] + c[1] * c[1];
const D = 2 * (a[0] * (b[1] - c[1]) + b[0] * (c[1] - a[1]) + c[0] * (a[1] - b[1]));
return [
1 / D * (ad * (b[1] - c[1]) + bd * (c[1] - a[1]) + cd * (a[1] - b[1])),
1 / D * (ad * (c[0] - b[0]) + bd * (a[0] - c[0]) + cd * (b[0] - a[0])),
];
}
};
const forEachVoronoiEdge = (pts, del, cb) => {
if (del) {
const { triangles, halfedges } = del;
const len = triangles.length;
for (let e = 0; e < len; e++) {
if (e < halfedges[e]) {
const p = triangleCenter(pts, del, triangleOfEdge(e));
const q = triangleCenter(pts, del, triangleOfEdge(halfedges[e]));
cb(e, p, q);
}
}
}
};
import * as scrawl from '../source/scrawl.js';
import { reportSpeed, addImageDragAndDrop } from './utilities.js';
const canvas = scrawl.findCanvas('mycanvas');
Namespacing boilerplate
const namespace = canvas.name;
const name = (n) => `${namespace}-${n}`;
Import image from DOM, and create Picture entity using it
scrawl.importDomImage('.flowers');
Magic number - base Cell dimensions
const baseDimension = 400;
Create the coordinates to be used as the Voronoi web’s points - for this demo these coordinates will be static, except for the first point which will track the mouse cursor position over the canvas
const coordArray = [],
center = [baseDimension/2, baseDimension/2];
for (let i = 0; i < 200; i++) {
const x = Math.floor(Math.random() * (baseDimension * 2) - (baseDimension / 2));
const y = Math.floor(Math.random() * (baseDimension * 2) - (baseDimension / 2));
coordArray.push([x, y]);
}
We build the Voronoi web in a RawAsset wrapper
const myAsset = scrawl.makeRawAsset({
name: name('voronoi-web'),
userAttributes: [{
points - an array holding the coordinate arrays we generate elsewhere
key: 'points',
defaultValue: [],
setter: function (item) {
/** @ts-expect-error */
this.points = [...item];
},
},{
here - a handle to our Canvas wrapper’s base Cell’s here
object, which gives us the current mouse cursor coordinates
key: 'here',
defaultValue: null,
},{
delaunay - a handle to the current Delaunator object we recreate on each update
key: 'delaunay',
defaultValue: null,
},{
canvasWidth, canvasHeight - make the RawAsset’s dimensions the same as our canvas base Cell’s dimensions
key: 'canvasWidth',
defaultValue: baseDimension,
},{
key: 'canvasHeight',
defaultValue: baseDimension,
},{
trigger - we update the RawAsset at the start of each Display cycle by setting its trigger
attribute. All the work with recreating the Delaunator object happens here
key: 'trigger',
defaultValue: false,
setter: function (item) {
/** @ts-expect-error */
const { points, here } = this;
if (here && here.active) points[0] = [here.x, here.y];
else points[0] = [...center];
/** @ts-expect-error */
this.delaunay = Delaunator.from(points);
/** @ts-expect-error */
this.dirtyData = item;
},
}],
assetWrapper
is the same as this
when function is declared with the function keyword
updateSource: function (assetWrapper) {
const { element, engine, points, delaunay, canvasWidth, canvasHeight } = assetWrapper;
element.width = canvasWidth;
if (!element.height) element.height = canvasHeight;
engine.strokeStyle = 'black';
engine.lineWidth = 2;
engine.beginPath();
forEachVoronoiEdge(points, delaunay, (e, p, q) => {
if (p && q) {
engine.moveTo(...p);
engine.lineTo(...q);
}
});
engine.stroke();
},
});
Initialize the RawAsset with relevant data
myAsset.set({
points: coordArray,
here: canvas.base.here,
});
The RawAsset needs a subscriber to make it active - currently filters do not subscribe to assets so we need to do it via an otherwise unused Picture entity
scrawl.makePicture({
name: name('temp'),
asset: name('voronoi-web'),
method: 'none',
});
We apply the mosaic effect over our image using a Scrawl-canvas filter
scrawl.makeFilter({
name: name('mosaic-filter'),
actions: [{
Load our RawAsset’s output - the Voronoi web - into the filter
action: 'process-image',
lineOut: 'web',
asset: name('voronoi-web'),
width: baseDimension,
height: baseDimension,
copyWidth: '100%',
copyHeight: '100%',
},{
Create the tiles from the Voronoi web
action: 'flood',
lineOut: 'white-background',
red: 255,
green: 255,
blue: 255,
alpha: 255,
},{
action: 'compose',
lineIn: 'web',
lineMix: 'white-background',
lineOut: 'webbed-background',
},{
action: 'channels-to-alpha',
lineIn: 'webbed-background',
lineOut: 'webbed-background',
includeRed: true,
includeGreen: false,
includeBlue: false,
},{
Apply our image over the tiles
action: 'compose',
lineIn: 'source',
lineMix: 'webbed-background',
compose: 'source-atop',
},{
Tile shadows - blur the voronoi web then apply it, offset up and left, to the image
action: 'gaussian-blur',
lineIn: 'web',
lineOut: 'blurred-web',
radius: 3,
},{
action: 'compose',
lineMix: 'blurred-web',
offsetX: -2,
offsetY: -2,
compose: 'destination-atop',
},{
Tile highlights - invert the blurred web then apply it, offset down and right, to the image
action: 'invert-channels',
lineIn: 'blurred-web',
lineOut: 'blurred-web',
},{
action: 'compose',
lineMix: 'blurred-web',
offsetX: 2,
offsetY: 2,
compose: 'destination-atop',
}],
});
Display our image in a Picture entity - the filter is applied here
const piccy = scrawl.makePicture({
name: name('image'),
asset: 'iris',
dimensions: ['100%', '100%'],
copyDimensions: ['100%', '100%'],
filters: [name('mosaic-filter')],
});
Function to display frames-per-second data, and other information relevant to the demo
const report = reportSpeed('#reportmessage');
Create the Display cycle animation
scrawl.makeRender({
name: name('animation'),
target: canvas,
We need to update our RawAsset at the start of each Display cycle
commence: () => myAsset.set({ trigger: true }),
afterShow: report,
});
scrawl.addNativeListener(['touchmove'], (e) => {
e.preventDefault();
e.returnValue = false;
}, canvas.domElement);
addImageDragAndDrop(canvas, `#${namespace} .assets`, piccy);
console.log(scrawl.library);