/** * WorkSpace class v0.1.0 * * @author coder-xiaomo * @date 2022-05-15 * * Released under the MIT license */ class WorkSpace { static settings = null; static primaryCanvas = null; constructor(settings) { this.settings = settings settings.workSpace = this // 清除原有内容 settings.container .style("width", settings.width) .style("height", settings.height) // .attr("width", settings.width) // .attr("height", settings.height) .html("") // 创建工作区SVG this.primaryCanvas = settings.container.append("svg") // 添加id .attr("id", "primaryCanvas") // 设置 SVG 宽高 .attr("width", "100%") .attr("height", "100%") // 背景色 .style("background-color", settings.colorMap["background"]) } } class ViBase { static workSpace = null; constructor(workSpace) { this.workSpace = workSpace } } class ArrayVi extends ViBase { // [47, 11, 50, 13, 16, 49, 8, 9, 38, 27, 20] // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] static listData // 网页加载完毕初始化事件 initialize({ elementId }) { let controlDiv = document.getElementById("control-div") controlDiv.style.textAlign = "center" var sortClassList = getSortClassList(); console.log(sortClassList); var DOMFragment = document.createDocumentFragment() var selector = document.createElement("select") for (let i = 0; i < sortClassList.length; i++) { const sortClass = new sortClassList[i](animation) const sortClassInfo = sortClass.info() // 跳过未完成的算法 if (!sortClassInfo['available']) continue let option = document.createElement("option") option.value = sortClassInfo['name'] option.innerText = sortClassInfo['name'] selector.appendChild(option) } DOMFragment.appendChild(selector) let ctrlBtn = document.createElement("button") ctrlBtn.innerHTML = "开始排序" let that = this ctrlBtn.onclick = function () { // 点击排序算法按钮 // if (!that.updateListDataArray(elementId, { doNotAlert: false })) // return if (!that.listData || that.listData.length == 0) { alert("数组为空") return } // 隐藏一些东西,显示一些东西 controlDiv.style.display = 'none' d3.select("#console-div") .style("display", "") d3.select("#console-current-algorithm") .style("text-align", "center") .html(selector.value/*sortClassInfo['name']*/) // 找到对应的算法,然后开始排序 for (let i = 0; i < sortClassList.length; i++) { const sortClass = new sortClassList[i](animation) const sortClassInfo = sortClass.info() if (sortClassInfo['name'] === selector.value) { sortClass.doSortWithAnimation(elementId) break } } } DOMFragment.appendChild(ctrlBtn) // 页面最后更新时间 var lastModifiedTime = (new Date(document.lastModified).getTime() / 1000).toFixed(0) let lastModifiedDiv = document.createElement("div") lastModifiedDiv.style.fontSize = "x-small" lastModifiedDiv.style.opacity = ".5" lastModifiedDiv.innerHTML = "页面版本戳: " + lastModifiedTime DOMFragment.appendChild(lastModifiedDiv) controlDiv.appendChild(DOMFragment) // 生成一个随机数组 this.randomListDataArray(elementId) // 显示 siderbar d3.select("#sidebar").style("display", "") } // 将数组显示到输入框中 updateListDataInput() { document.getElementById("array-input").value = this.listData.join(",") } // Array 内容改变时 updateListDataArray(elementId, { doNotAlert = false }) { var val = document.getElementById("array-input").value try { var preList = val.replaceAll(",", ",").split(",") this.listData = [] preList.forEach(element => { if (element.trim() === "" || isNaN(element)) return this.listData.push(Number(element)) }); arrayVi.initArray(elementId, this.listData) } catch (err) { console.log(err) if (!doNotAlert) alert("输入不正确,请检查!") return false } return true } // 随机 Array randomListDataArray(elementId) { function getRandom(length) { return Math.floor(Math.random() * length); // 可均衡获取 0 到 length-1 的随机整数。 } // 获取一个 6 - 12 以内的随机数 var len = 6 + getRandom(13 - 6) this.listData = [] for (let i = 0; i < len; i++) { this.listData.push(getRandom(51)) } this.updateListDataInput() this.updateListDataArray(elementId, { doNotAlert: false }) } // 绘制数组 initArray(elementId, listData) { console.log("initArray") let fragment = shape.getLinkedListFragment(elementId, listData, { x: 100, y: 100, width: "100px", height: "100px", }) // console.log(fragment) workSpace.primaryCanvas.html("") // 添加水印 居中 var watermarkWidth = settings.outerSize.height * 0.65 shape.addWatermark(elementId, { imageSrc: "./assets/image/logo-small.svg", }) .attr('id', 'watermark-c-c') .attr('x', settings.outerSize.width / 2) .attr('y', settings.outerSize.height / 2) .style('width', watermarkWidth + 'px') .style('height', watermarkWidth + 'px') .style('transform', `translate(-${watermarkWidth / 2}px, -${watermarkWidth / 2}px)`) .style('opacity', '0.015') .style('transition', '0.2s') // 添加水印 右下角 var watermarkWidth = 60 shape.addWatermark(elementId, { imageSrc: "./assets/image/logo-small.svg", }) .attr('id', 'watermark-r-b') .attr('x', settings.outerSize.width) .attr('y', settings.outerSize.height) .style('width', watermarkWidth + 'px') .style('height', watermarkWidth + 'px') .style('transform', 'translate(-80px, -80px)') workSpace.primaryCanvas.node().appendChild(fragment) document.getElementById(elementId).customAttr = fragment.customAttr } } /** * Shape class v0.1.0 * * @author coder-xiaomo * @date 2022-05-15 * * Released under the MIT license */ class Shape { static workSpace = null; constructor(workSpace) { this.workSpace = workSpace } addShape(shape, id) { var settings = this.workSpace.settings return workSpace.primaryCanvas.append(shape) .style("transform", `translate(${settings.margin.left}px, ${settings.margin.top}px)`) .attr("id", id) } addShape_NoTransform(shape, id) { return workSpace.primaryCanvas.append(shape) .attr("id", id) } // 添加矩形 rectangle(id, { x, y, width, height, fillColor = "white", strokeColor = "black" }) { return this.addShape("rect", id) .attr("x", x) .attr("y", y) .attr("width", width) .attr("height", height) .style("fill", fillColor) .style("stroke", strokeColor) } // 添加圆形 circle(id, { x, y, radius, fillColor = "white", strokeColor = "black" }) { return this.addShape("circle", id) .attr("cx", x) .attr("cy", y) .attr("r", radius) .style("fill", fillColor) .style("stroke", strokeColor) } // 添加文本 text(id, { x, y, text, fillColor }) { return this.addShape("text", id) .attr("x", x) .attr("y", y) .text(text) .style("fill", fillColor) } // 添加线 line(id, { x1, y1, x2, y2, strokeColor }) { return this.addShape("line", id) .attr("x1", x1) .attr("y1", y1) .attr("x2", x2) .attr("y2", y2) .style("stroke", strokeColor) } // 添加路径 path(id, d, { fillColor = "white", strokeColor = "black" }) { return this.addShape("path", id) .attr("d", d) .style("fill", fillColor) .style("stroke", strokeColor) } // 添加坐标轴 axis(id, { transform, axis }) { return this.addShape_NoTransform("g", id) .attr("transform", transform) .call(axis) } // 添加一个链表节点 addNode(id, x, y, width, height, text) { var primaryCanvas = workSpace.primaryCanvas primaryCanvas.append("rect", id) .attr("x", x) .attr("y", y) .attr("width", width) .attr("height", height) .style("fill", workSpace.settings.colorMap["fill"]) .style("stroke", workSpace.settings.colorMap["stroke"]) primaryCanvas.append("text", id + "_text") .attr("x", x) .attr("y", y) // .attr("x", x + width / 2) // .attr("y", y + height / 2) .text(text) .style("fill", workSpace.settings.colorMap["text"]) } // 添加一个图片水印 addWatermark(id, { imageSrc }) { var primaryCanvas = workSpace.primaryCanvas return primaryCanvas.append("image", id) .attr("xlink:href", imageSrc) } // 绘制一个链表 getLinkedListFragment(id, nodes) { var settings = this.workSpace.settings let displayMaxWidth = 1 * settings.innerSize.width let displayMaxHeight = 1 * settings.innerSize.height let areaWidth = 1 * nodes.length // 按照1个Unit来计算 let areaHeight = 1 // 按照1个Unit来计算 let oneUnit = 100 // 可以假设高度相等比较宽度,这样好理解 if (displayMaxWidth / displayMaxHeight > areaWidth / areaHeight) { // 展示区域左右有多余空间 oneUnit = displayMaxHeight / areaHeight } else { // 展示区域上下有空间(或刚刚好) oneUnit = displayMaxWidth / areaWidth } // 定义最大值 if (oneUnit > 120) oneUnit = 120 let fragment = document.createDocumentFragment() fragment.customAttr = { id: id, nodes: nodes, type: "linkedList", oneUnit: oneUnit, gsapTimeline: gsap.timeline({ onStart: function () { consoleClear() }, onComplete: function () { consoleLog(`排序完成`) console.log("all done") // this.seek(0) } }) } // console.log(fragment.customAttr) // 元素不能设置 width 和 height let g = d3.select(fragment) .append("svg:g") .attr("id", id) .attr("fill", "white") .attr("transform", `translate(${settings.margin.left}, ${settings.margin.top})`) for (let i = 0; i < nodes.length; i++) { let node = nodes[i] let _g = g.append("svg:g") _g.append("svg:rect") .attr("x", i * oneUnit) .attr("y", 0) .attr("width", oneUnit) .attr("height", oneUnit) .attr("fill", settings.colorMap["fill"]) .attr("stroke", settings.colorMap["stroke"]) _g.append("svg:text") .text(node) .attr("x", i * oneUnit + oneUnit / 2) .attr("y", oneUnit / 2) .attr("width", oneUnit) .attr("height", oneUnit) .attr("fill", settings.colorMap["text"]) // 调试用 if (settings.debugMode) _g.append("svg:text") .text(i) .attr("x", i * oneUnit) .attr("y", oneUnit) .attr("width", oneUnit) .attr("height", oneUnit) .attr("fill", "black") // console.log(text.node().getBBox()) // console.log(text.node().getBoundingClientRect()) } // g.append("svg:rect") // .attr("width", oneUnit * nodes.length) // .attr("height", oneUnit) // .style("fill", "none") // .style("stroke", "green") return fragment } } class VectorAnimation { constructor(workSpace) { this.workSpace = workSpace } swapElementAttr(attrName/* or attrNameList */, element1, element2) { function exchange(attrName) { // 保存 element1 的属性,将 element2 的属性赋值给 element1, 再将保存的属性赋值给 element2 var tmp = element1.getAttribute(attrName) element1.setAttribute(attrName, element2.getAttribute(attrName)) element2.setAttribute(attrName, tmp) } if (typeof attrName === "string") { exchange(attrName) } else if (typeof attrName === "object") { for (let i = 0; i < attrName.length; i++) { exchange(attrName[i]) } } } swapElementInnerHTML(element1, element2) { var tmp = element1.innerHTML element1.innerHTML = element2.innerHTML element2.innerHTML = tmp } swapElementsAttr(elementPairList) { // [ // [attrName or attrNameList, element1, element2], // [attrName or attrNameList, element1, element2], // ... // ] for (let i = 0; i < elementPairList.length; i++) { let elementPair = elementPairList[i] this.swapElementAttr(elementPair[0], elementPair[1], elementPair[2]) } } // 交换数组元素 swapLinkedListItems(id, [fromIndex, toIndex]) { if (fromIndex < toIndex) [fromIndex, toIndex] = [toIndex, fromIndex] var settings = this.workSpace.settings let linkedList = document.getElementById(id) let customAttr = linkedList.customAttr // console.log(customAttr) var gList = linkedList.childNodes let from = gList[fromIndex] let to = gList[toIndex] var deltaX = customAttr.oneUnit * (fromIndex - toIndex); var deltaY = customAttr.oneUnit * 1.08 var animateSettings = this.workSpace.settings.animation.getConf() // 如果是相邻的两个元素交换,或者要交换的两个元素是同一个元素 if (Math.abs(fromIndex - toIndex) <= 1) { deltaY /= 2 animateSettings.duration *= 0.6 } var that = this let timeline = gsap.timeline({ onStart: function () { displayCurrentArray(customAttr.nodes) consoleLog(`交换索引为 ${fromIndex} 和 ${toIndex} 的两个元素`) }, onComplete: function () { // 交换DOM元素中的值 that.swapElementInnerHTML(from.childNodes[1], to.childNodes[1]) // 交换 node 列表中的值 // console.log(customAttr.nodes) let tmp = customAttr.nodes[fromIndex] customAttr.nodes[fromIndex] = customAttr.nodes[toIndex] customAttr.nodes[toIndex] = tmp // console.log(customAttr.nodes) displayCurrentArray(customAttr.nodes) console.log("animation done (swap)") } }).add([ gsap.to(from.childNodes[0], { ...animateSettings, fill: settings.colorMap["fill_focus"] }), gsap.to(to.childNodes[0], { ...animateSettings, fill: settings.colorMap["fill_focus"] }), gsap.to(from.childNodes[1], { ...animateSettings, fill: settings.colorMap["text_focus"] }), gsap.to(to.childNodes[1], { ...animateSettings, fill: settings.colorMap["text_focus"] }), gsap.to(from.childNodes, { ...animateSettings, y: -deltaY }), gsap.to(to.childNodes, { ...animateSettings, y: deltaY }), ]).add([ gsap.to(from.childNodes, { ...animateSettings, x: -deltaX }), gsap.to(to.childNodes, { ...animateSettings, x: deltaX }), ]).add([ gsap.to(from.childNodes[0], { ...animateSettings, fill: settings.colorMap["fill"] }), gsap.to(to.childNodes[0], { ...animateSettings, fill: settings.colorMap["fill"] }), gsap.to(from.childNodes[1], { ...animateSettings, fill: settings.colorMap["text"] }), gsap.to(to.childNodes[1], { ...animateSettings, fill: settings.colorMap["text"] }), gsap.to(from.childNodes, { ...animateSettings, y: 0 }), gsap.to(to.childNodes, { ...animateSettings, y: 0 }), ]).add([ // 恢复到动画之前的状态 gsap.to(from.childNodes, { ...animateSettings, duration: 0, x: 0, y: 0 }), gsap.to(to.childNodes, { ...animateSettings, duration: 0, x: 0, y: 0 }), ]) customAttr.gsapTimeline.add(timeline) } // 高亮数组元素 highlightLinkedListItems(id, indexList, onStartCallback) { let linkedList = document.getElementById(id) let customAttr = linkedList.customAttr var gList = linkedList.childNodes if (typeof (indexList) === "number") indexList = [indexList] var hightlightElementList_fill = [] var hightlightElementList_text = [] for (let i = 0; i < indexList.length; i++) { const index = indexList[i]; hightlightElementList_fill.push(gList[index].childNodes[0]) hightlightElementList_text.push(gList[index].childNodes[1]) } var animateSettings = this.workSpace.settings.animation.getConf() let timeline = gsap.timeline({ onStart: function () { displayCurrentArray(customAttr.nodes) if (onStartCallback) onStartCallback() }, onComplete: function () { console.log("animation done (hightlight)") } }).add([ gsap.to(hightlightElementList_fill, { ...animateSettings, fill: settings.colorMap["fill_focus"] }), gsap.to(hightlightElementList_text, { ...animateSettings, fill: settings.colorMap["text_focus"] }), ]).add([ gsap.to(hightlightElementList_fill, { ...animateSettings, fill: settings.colorMap["fill"] }), gsap.to(hightlightElementList_text, { ...animateSettings, fill: settings.colorMap["text"] }), ]) customAttr.gsapTimeline.add(timeline) } // 比较数组元素 compareLinkedListItems(id, index1, index2) { this.highlightLinkedListItems(id, [index1, index2], function () { consoleLog(`比较索引为 ${index1} 和 ${index2} 的两个元素`) }) } // 弹出/弹回 数组元素 popupLinkedListItems(id, index, { popBack = false }) { let linkedList = document.getElementById(id) let customAttr = linkedList.customAttr var gList = linkedList.childNodes var deltaY = customAttr.oneUnit * 1.08 if (popBack) deltaY = 0 let timeline = gsap.timeline({ onStart: function () { if (typeof (onStartCallback) === "function") onStartCallback() }, onComplete: function () { console.log("animation done (popup)") } }).add([ gsap.to(gList[index], { ...this.workSpace.settings.animation.getConf(), y: deltaY }), ]) customAttr.gsapTimeline.add(timeline) } // 强调/恢复 数组元素 emphasisLinkedListItems(id, index, { popBack = false }) { let linkedList = document.getElementById(id) let customAttr = linkedList.customAttr var gList = linkedList.childNodes let eles = [] if (typeof (index) === "object") { for (let i = 0; i < index.length; i++) { eles.push(gList[index[i]]) } } else { eles.push(gList[index]) } let timeline = gsap.timeline({ onStart: function () { if (typeof (onStartCallback) === "function") onStartCallback() }, onComplete: function () { console.log("animation done (popup)") } }).add( popBack == false ? [ gsap.to(eles, { ...this.workSpace.settings.animation.getConf(), y: customAttr.oneUnit * 0.4, fill: settings.colorMap["fill_focus"] }), ] : [ gsap.to(eles, { ...this.workSpace.settings.animation.getConf(), y: 0, fill: settings.colorMap["fill"] }), ] ) customAttr.gsapTimeline.add(timeline) } // 交换相邻数组元素位置(仅在水平方向呼唤,垂直方向不做调整) 【fromIndex 是y方向上突出的元素】 exchangeLinkedListItems(id, fromIndex, toIndex) { if (fromIndex < toIndex) [fromIndex, toIndex] = [toIndex, fromIndex] var settings = this.workSpace.settings let linkedList = document.getElementById(id) let customAttr = linkedList.customAttr // console.log(customAttr) var gList = linkedList.childNodes let from = gList[fromIndex] let to = gList[toIndex] var deltaX = customAttr.oneUnit * (fromIndex - toIndex); var deltaY = customAttr.oneUnit * 1.08 // 要跟 popupLinkedListItems 函数中的 deltaY 完全一致 var animateSettings = settings.animation.getConf() var that = this let timeline = gsap.timeline({ onStart: function () { displayCurrentArray(customAttr.nodes) consoleLog(`交换索引为 ${fromIndex} 和 ${toIndex} 的两个元素`) }, onComplete: function () { // 交换DOM元素中的值 that.swapElementInnerHTML(from.childNodes[1], to.childNodes[1]) // 交换 node 列表中的值 // console.log(customAttr.nodes) let tmp = customAttr.nodes[fromIndex] customAttr.nodes[fromIndex] = customAttr.nodes[toIndex] customAttr.nodes[toIndex] = tmp // console.log(customAttr.nodes) displayCurrentArray(customAttr.nodes) console.log("animation done (exchange)") } }).add([ gsap.to(from.childNodes, { ...animateSettings, x: -deltaX }), gsap.to(to.childNodes, { ...animateSettings, x: deltaX }), ]).add([ // 恢复到动画之前的状态 gsap.to(from.childNodes, { ...animateSettings, duration: 0, x: 0 }), gsap.to(to.childNodes, { ...animateSettings, duration: 0, x: 0 }), // 由于两个元素一个突出,一个不突出,所以还要交换两个y gsap.to(from, { ...animateSettings, duration: 0, y: 0 }), gsap.to(to, { ...animateSettings, duration: 0, y: deltaY }), ]) customAttr.gsapTimeline.add(timeline) } }