Carrot
https://carrotsearch.com/
FoamTree
https://carrotsearch.com/foamtree/
FoamTree demo
https://get.carrotsearch.com/foamtree/latest/demos/
胡蘿蔔(Carrot)為文檔聚類分析平臺,通過其非常強大的聚類演算法將結果視覺化為泡沫樹(foamTree),常用於聚類分析與佔比視覺化,例如webpack打包後的chunks大小分析視覺化即為Carrot的FoamTree。
以下為單html檔測試範例(FoamTree圖)︰
載入後畫面為︰
#Javascript, Carrot, FoamTree, 泡沫樹
https://carrotsearch.com/
FoamTree
https://carrotsearch.com/foamtree/
FoamTree demo
https://get.carrotsearch.com/foamtree/latest/demos/
胡蘿蔔(Carrot)為文檔聚類分析平臺,通過其非常強大的聚類演算法將結果視覺化為泡沫樹(foamTree),常用於聚類分析與佔比視覺化,例如webpack打包後的chunks大小分析視覺化即為Carrot的FoamTree。
以下為單html檔測試範例(FoamTree圖)︰
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <title>foamtree polygon Demo</title> <!--使用lodash--> <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script> <!--使用foamtree--> <script src="https://get.carrotsearch.com/foamtree/latest/carrotsearch.foamtree.js"></script> <style> body { background-color: black; font-family: Arial; } #app { position: absolute; top: 0; bottom: 0; left: 0; right: 0; } </style> </head> <body> <div id="app"></div> <script> let imgs = [ 'https://images.freeimages.com/images/large-previews/8a1/small-waterfall-1376352.jpg', 'https://images.freeimages.com/images/large-previews/c53/yellowstone-river-1361768.jpg', 'https://images.freeimages.com/images/large-previews/f2c/effi-1-1366221.jpg', 'https://images.freeimages.com/images/large-previews/9f9/selfridges-2-1470748.jpg', 'https://images.freeimages.com/images/large-previews/fa7/in-prayer-1313108.jpg', 'https://images.freeimages.com/images/large-previews/371/swiss-mountains-1362975.jpg', 'https://images.freeimages.com/images/large-previews/535/natural-wonders-1400924.jpg', 'https://images.freeimages.com/images/large-previews/f5d/butterfly-1378183.jpg', 'https://images.freeimages.com/images/large-previews/e2a/boise-downtown-1387405.jpg', 'https://images.freeimages.com/images/large-previews/8a5/red-tulip-2-1401227.jpg', 'https://images.freeimages.com/images/large-previews/bf4/the-road-through-the-woods-1449194.jpg', 'https://images.freeimages.com/images/large-previews/4dc/street-1366583.jpg', 'https://images.freeimages.com/images/large-previews/977/beach-1364350.jpg', 'https://images.freeimages.com/images/large-previews/07a/beach-soft-light-1379401.jpg', 'https://images.freeimages.com/images/large-previews/815/xmas-bunny-1313404.jpg', 'https://images.freeimages.com/images/large-previews/ed3/a-stormy-paradise-1-1563744.jpg', 'https://images.freeimages.com/images/large-previews/fb3/grass-1379193.jpg', 'https://images.freeimages.com/images/large-previews/56e/hibiscus-1393855.jpg', 'https://images.freeimages.com/images/large-previews/6d5/lake-at-the-cottage-1372381.jpg', 'https://images.freeimages.com/images/large-previews/3a6/rain-on-sea-ii-1368899.jpg', 'https://images.freeimages.com/images/large-previews/e71/frog-1371919.jpg', 'https://images.freeimages.com/images/large-previews/b5a/dragon-fly-1391358.jpg', 'https://images.freeimages.com/images/large-previews/901/butterfly-dress-1520606.jpg', 'https://images.freeimages.com/images/large-previews/39a/no-grain-no-pain-1326753.jpg', 'https://images.freeimages.com/images/large-previews/56c/salat-tomato-cheese-on-bread-1322759.jpg', 'https://images.freeimages.com/images/large-previews/503/fruit-in-the-country-3-1323660.jpg', 'https://images.freeimages.com/images/large-previews/bf2/fields-1-1370990.jpg', 'https://images.freeimages.com/images/large-previews/e93/skyscrapers-1219500.jpg', 'https://images.freeimages.com/images/large-previews/773/koldalen-4-1384902.jpg', 'https://images.freeimages.com/images/large-previews/8b3/palm-and-pier-1390929.jpg', 'https://images.freeimages.com/images/large-previews/5a3/milky-droplet-2-1190386.jpg', 'https://images.freeimages.com/images/large-previews/4ad/snare-drum-second-take-1-1564542.jpg', 'https://images.freeimages.com/images/large-previews/4f3/verde-que-te-quiero-verde-1-1408392.jpg', 'https://images.freeimages.com/images/large-previews/313/coffee-1559191.jpg', 'https://images.freeimages.com/images/large-previews/8a8/yellow-galben-1518220.jpg', 'https://images.freeimages.com/images/large-previews/e7e/orange-portocaliu-1518994.jpg', 'https://images.freeimages.com/images/large-previews/6bd/laundry-1522596.jpg', 'https://images.freeimages.com/images/large-previews/2c8/glassy-1538117.jpg', 'https://images.freeimages.com/images/large-previews/098/naive-art-paintings-naife-artworks-painting-raphael-perez-painter-landscape-artowkrs-1637257.jpg', 'https://a-z-animals.com/media/animals/images/470x370/frog4.jpg', 'https://www.spirit-animals.com/wp-content/uploads/2013/03/Frog-2-1024x768.jpg', 'https://images.pexels.com/photos/162140/duckling-birds-yellow-fluffy-162140.jpeg?cs=srgb&dl=animal-beak-bird-162140.jpg&fm=jpg', 'https://animals.sandiegozoo.org/sites/default/files/2016-08/category-thumbnail-mammals_0.jpg', 'https://ichef.bbci.co.uk/images/ic/976x549/p04f5x5v.jpg', 'https://cdn.theatlantic.com/assets/media/img/mt/2018/10/GettyImages_939413302/lead_720_405.jpg?mod=1541011155', 'https://www.rspcansw.org.au/wp-content/uploads/2017/04/7_a-feature_adopt_mobile-1.jpg', 'https://www.iata.org/whatwedo/cargo/PublishingImages/cargo_live_animals_parrot.jpg', 'https://www.marwell.org.uk/images/main-images/white-rhino.jpg', 'https://www.gvi.co.uk/wp-content/uploads/2016/05/2-1.png', 'https://www.mlar.org/media/1165/dustin-1.jpg', 'https://www.rd.com/wp-content/uploads/2018/06/Deer-fawn-portrait-760x506.jpg', ] //groups let groups = _.map(imgs, function (src) { let title = _.split(src, '/').pop() let group = { //title title: title, //per-group random number random: 0.8 + 0.2 * Math.random(), //Low-resolution image image: undefined, imageLoaded: false, //true when image has just been loaded imageLoadedTime: undefined, //time the image completed loading, used for fading-in animation //High-resolution image hiresImage: undefined, //True when some image-specific data is loading loading: false, //Low-resolution image loaded with CORS headers crossOriginImage: undefined }; //load low-resolution image let img = new Image(); img.onload = function () { group.image = img; group.imageLoaded = true; group.imageLoadedTime = Date.now(); foamtree.redraw(false, src); }; img.src = src return group; }) </script> <script> //foamtree let foamtree = new CarrotSearchFoamTree({ //id id: "app", //pixelRatio pixelRatio: window.devicePixelRatio || 1, //Use a simple fading animation rolloutDuration: 0, pullbackDuration: 0, fadeDuration: 300, //Make the polygon fill entire viewport when exposed groupExposureZoomMargin: 0.02, //Draw the attribution in dark colors to match the theme of this demo attributionTheme: "dark", //Draw custom content during animations wireframeContentDecorationDrawing: "always", //The decorator that draws our images and loading spinner animation. groupContentDecorator: function (opts, props, vars) { //The canvas 2d context on which we'll be drawing let ctx = props.context; //The group we're requested to draw. let group = props.group; //Current time, we'll need it to draw animations let now = Date.now(); //Don't draw default labels and polygons vars.groupLabelDrawn = false; vars.groupPolygonDrawn = false; //Fading-in of the image that was just loaded and fading-out of the loading spinner animation. let imageAlpha = 0; if (group.image) { //Image is available, fade it in imageAlpha = Math.min(1, (now - group.imageLoadedTime) / 300); ctx.globalAlpha = imageAlpha; drawImage(); } if (imageAlpha < 1 || group.loading) { //Image still loading of fading-in, draw spinner animation. ctx.globalAlpha = group.loading ? 1.0 : 1 - imageAlpha; drawSpinner(); //Schedule a redraw of this group. foamtree.redraw(false, group); } //Draws the loading spinner animation function drawSpinner() { let cx = props.polygonCenterX; let cy = props.polygonCenterY; if (props.shapeDirty) { //If group's polygon changed, recompute the radius of the inscribed polygon. group.spinnerRadius = CarrotSearchFoamTree.geometry.circleInPolygon(props.polygon, cx, cy) * 0.1; } // Draw the spinner. Advance the animation based on the current time. let angle = 2 * Math.PI * (now % 1000) / 1000; ctx.beginPath(); ctx.arc(cx, cy, group.spinnerRadius, angle, angle + Math.PI / 5, true); ctx.strokeStyle = "white"; ctx.lineWidth = group.spinnerRadius * 0.3; ctx.stroke(); } //Draws the image in the group's polygon. function drawImage() { // If the group's polygon changed or image has just loaded, recompute the geometry-dependent elements. if (props.shapeDirty || group.imageLoaded) { group.imageLoaded = false; //Bounding box of the polygon group.boundingBox = CarrotSearchFoamTree.geometry.boundingBox(props.polygon); //Rectangle inscribed in the polygon. We'll set the aspect ratio of the rectangle to be the //same as the aspect ratio of the image. When the group is exposed, we'll draw the full //image in the inscribed rectangle. group.inscribedBox = CarrotSearchFoamTree.geometry.rectangleInPolygon( props.polygon, props.polygonCenterX, props.polygonCenterY, group.image.width / group.image.height, 0.95); //Check if there's enough space for the label. If not, shift the inscribed box upwards a bit. let descriptionHeight = group.boundingBox.y + group.boundingBox.h - group.inscribedBox.y - group.inscribedBox.h; let minDescriptionHeight = 0.125 * group.boundingBox.h; if (descriptionHeight < minDescriptionHeight) { group.inscribedBox = CarrotSearchFoamTree.geometry.rectangleInPolygon( props.polygon, props.polygonCenterX, props.polygonCenterY - (minDescriptionHeight - descriptionHeight), group.image.width / group.image.height, 0.95); } //Clear the label buffer. We'll lay out the label when needed. group.labelBuffer = null; } //Choose the right resolution image. We randomize the point at which we switch to the high-res image for each group let image; if (group.hiresImage && (group.boundingBox.w * props.viewportScale * group.random > group.image.width || group.boundingBox.h * props.viewportScale * group.random > group.image.height)) { image = group.hiresImage; } else { image = group.image; } //animate the image rectangle during the expose animation. let mainImageBox; let exposure = props.exposure; if (exposure <= 0) { //Not exposed, render cropped image mainImageBox = group.boundingBox; } else if (exposure == 1) { //Exposed, render full image mainImageBox = group.inscribedBox; } else { //Expose animation in progress, transition the image rectangle geometry. mainImageBox = { x: group.boundingBox.x * (1 - exposure) + group.inscribedBox.x * exposure, y: group.boundingBox.y * (1 - exposure) + group.inscribedBox.y * exposure, w: group.boundingBox.w * (1 - exposure) + group.inscribedBox.w * exposure, h: group.boundingBox.h * (1 - exposure) + group.inscribedBox.h * exposure }; } //Set the group polygon path on the drawing context. ctx.beginPath(); props.polygonContext.replay(ctx); ctx.closePath(); //Since the image is larger than the polygon, we'll need to apply clipping so that we don't draw beyond the polygon's area. ctx.save(); ctx.clip(); //If the group is exposed, draw full image and the blurred backdrop. if (exposure > 0) { // Make the backdrop a bit darker. ctx.fillStyle = "#000000"; ctx.fill(); //If blurred image is not available, draw a heavily cropped original image as the background and make it even darker. ctx.save(); ctx.globalAlpha *= group.blurredImage ? 0.6 : 0.3; drawImageInBox(group.blurredImage || group.image, group.boundingBox, group.blurredImage ? undefined : 2); ctx.restore(); //When the expose animation is almost complete, draw some basic info about the photo. if (exposure > 0.9) { //re-layout text (which is costly) only when the geometry of the polygon changes. if (!group.labelBuffer) { //draw title let title = group.title //Create the label buffer group.labelBuffer = ctx.scratch(); //Put the title line directly below the image rectangle. let info = group.labelBuffer.fillPolygonWithText(props.polygon, props.polygonCenterX, group.inscribedBox.y + group.inscribedBox.h, title, { verticalAlign: "top", maxFontSize: group.boundingBox.h / 30, fontFamily: "Arial" }); if (info.fit) { // If there was enough space for the title, lay out author and views information. // Put those directly below the title. group.labelBuffer.save(); group.labelBuffer.globalAlpha = 0.6; group.labelBuffer.fillPolygonWithText(props.polygon, props.polygonCenterX, info.box.y + info.box.h, '', { verticalAlign: "top", maxFontSize: info.fontSize, fontFamily: "Arial", verticalPadding: 0 }); group.labelBuffer.restore(); } } //Fade-in the label ctx.fillStyle = "rgba(255, 255, 255, " + (8.5 * (exposure - 0.9)).toFixed(2) + ")"; group.labelBuffer.replay(ctx); } } //Draw the main image if (exposure > 0 || props.hovered) { drawImageInBox(image, mainImageBox); } else { ctx.save(); ctx.globalAlpha *= 0.9; drawImageInBox(image, mainImageBox); ctx.restore(); } ctx.restore(); //Draw a subtle polygon outline ctx.strokeStyle = props.exposure > 0 || props.hovered ? "rgba(255, 255, 255, 0.25)" : "rgba(0, 0, 0, 0.4)"; ctx.lineWidth = 1; ctx.stroke(); //Draws the image positioned in the provided rectangle. function drawImageInBox(image, box, multiplier) { let groupWidthToHeight = box.w / box.h; let imageWidthToHeight = image.width / image.height; let scale = groupWidthToHeight < imageWidthToHeight ? box.h / image.height : box.w / image.width; let xOffset = box.x / scale, yOffset = box.y / scale; if (groupWidthToHeight < imageWidthToHeight) { scale = box.h / image.height; xOffset -= (image.width - box.w / scale) / 2; } else { scale = box.w / image.width; yOffset -= (image.height - box.h / scale) / 2; } group.scale = scale; ctx.save(); ctx.scale(scale, scale); ctx.translate(xOffset, yOffset); if (multiplier) { ctx.translate(image.width / 2, image.height / 2); ctx.scale(multiplier, multiplier); ctx.translate(-image.width / 2, -image.height / 2); } ctx.drawImage(image, 0, 0); ctx.restore(); } } }, //Call group content decorator on every polygon draw. groupContentDecoratorTriggering: "onSurfaceDirty", //Expose group on single click onGroupClick: function (e) { e.preventDefault(); //Unexpose if clicked outside of any polygon if (!e.group) { this.set("exposure", null); return; } switch (this.get("state", e.group).exposure) { case -1: case 0: //Clicked group is not exposed: compute blurred image and expose. if (e.group.crossOriginImage && !e.group.blurredImage) { e.group.blurredImage = Blur.stackBlur(e.group.crossOriginImage, 16); } this.set("exposure", e.group); break; case 1: //Clicked group is exposed, unexpose. this.set("exposure", null); break; } }, //Prevent default action (expose) on double click onGroupDoubleClick: function (e) { e.preventDefault(); } }); //set foamtree.set("dataObject", { groups: groups }); //Resize on window size changes window.addEventListener("resize", function () { _.delay(function () { foamtree.resize() }, 300) }); </script> </body> </html>
載入後畫面為︰
#Javascript, Carrot, FoamTree, 泡沫樹
留言
張貼留言