/** * 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.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"]) } } /** * 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"]) } // 绘制一个链表 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) } // 交换相邻数组元素位置(仅在水平方向呼唤,垂直方向不做调整) 【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) } }