Збільшити масштаб точки (використовуючи масштаб та перекласти)


156

Я хочу мати можливість збільшити масштаб точки під мишею на полотні HTML 5, як-от масштабування на Картах Google . Як я можу цього досягти?


2
Я використовував це для збільшення масштабу полотна, і він чудово працює! Єдине, що мені потрібно додати, це те, що розрахунок суми збільшення не такий, як можна було б очікувати. "var zoom = 1 + колесо / 2;" тобто це призводить до 1,5 для збільшення та 0,5 для зменшення масштабу. Я відредагував це у своїй версії, щоб у мене було 1,5 для збільшення та 1 / 1,5 для зменшення масштабу, що дозволяє збільшити масштаб та зменшити масштаб. Тож якщо ви збільшуєте масштаб один раз і збільшуєте масштаб назад, ви матимете таку ж картину, що і перед збільшенням.
Кріс

2
Зауважте, що це не працює на Firefox, але метод можна легко застосувати до плагіну мишового колеса jQuery . Дякую, що поділились!
johndodo

2
var zoom = Math.pow (1,5f, колесо); // Використовуйте це для обчислення масштабу. Має перевагу, що масштабування на колесі = 2 збігається із збільшенням масштабу двічі за кермом = 1. Крім того, збільшення + і зменшення масштабу на +2 відновлює початкову шкалу.
Метт

Відповіді:


126

Краще рішення - просто перемістити положення вікна перегляду на основі зміни масштабу. Точка масштабування - це просто точка старого масштабу та нового масштабу, яка ви хочете залишитися такою ж. Що означає, що попередньо збільшений вікно перегляду та огляд перегляду зі збільшенням мають однакову точку масштабу відносно області перегляду. З огляду на те, що ми масштабуємо відносно походження. Ви можете відповідно відрегулювати положення вікна перегляду:

scalechange = newscale - oldscale;
offsetX = -(zoomPointX * scalechange);
offsetY = -(zoomPointY * scalechange);

Тож дійсно ви можете просто переміститися вниз і вправо, коли збільшуєте масштаб, на коефіцієнт, на який ви збільшили масштаб, по відношенню до точки, на яку ви збільшили масштаб.

введіть тут опис зображення


2
Більш цінним, ніж вирізати та вставити код, є пояснення того, що найкраще рішення, і чому він працює без багажу, особливо якщо він три довжини.
Татариз

2
scalechange = новина / oldscale?
Тееш Аліміллі

4
Крім того, я хотів би додати для тих, хто прагне створити карту, як компонент пан-масштабування, що миша X, Y повинна бути (mousePosRelativeToContainer - currentTransform) / currentScale, інакше вона буде розглядати поточну позицію миші відносно контейнера.
Гілад

1
Так, ця математика передбачає, що масштаб та панорама знаходяться у координатах, що відповідають походженню. Якщо вони відносяться до вікна перегляду, ви повинні їх відповідним чином відрегулювати. Хоча я б припустив, що правильною математикою є zoomPoint = (mousePosRelativeToContainer + currentTranslation). Ця математика також передбачає, що точка початку зазвичай знаходиться в лівій верхній частині поля. Але коригувати трохи нетипові ситуації набагато простіше, враховуючи простоту.
Tatarize

1
@ C.Finke Другий спосіб це зробити, використовуючи переклади в ctx. Ви малюєте все однакового розміру і в однакових положеннях. Але ви просто використовуєте множення матриць на полотні javascript, щоб встановити панораму та масштаб (масштаб) контексту. Тому замість того, щоб перемальовувати всі фігури в іншому місці. Ви малюєте їх там же, і переміщуєте вікно перегляду в javascript. Цей спосіб також вимагатиме від вас подій миші та перекладіть їх назад. Отже, ви віднімете панораму, а потім поверніть коефіцієнт на масштабі.
Tatarize

67

Нарешті вирішив це:

var zoomIntensity = 0.2;

var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var width = 600;
var height = 200;

var scale = 1;
var originx = 0;
var originy = 0;
var visibleWidth = width;
var visibleHeight = height;


function draw(){
    // Clear screen to white.
    context.fillStyle = "white";
    context.fillRect(originx,originy,800/scale,600/scale);
    // Draw the black square.
    context.fillStyle = "black";
    context.fillRect(50,50,100,100);
}
// Draw loop at 60FPS.
setInterval(draw, 1000/60);

canvas.onwheel = function (event){
    event.preventDefault();
    // Get mouse offset.
    var mousex = event.clientX - canvas.offsetLeft;
    var mousey = event.clientY - canvas.offsetTop;
    // Normalize wheel to +1 or -1.
    var wheel = event.deltaY < 0 ? 1 : -1;

    // Compute zoom factor.
    var zoom = Math.exp(wheel*zoomIntensity);
    
    // Translate so the visible origin is at the context's origin.
    context.translate(originx, originy);
  
    // Compute the new visible origin. Originally the mouse is at a
    // distance mouse/scale from the corner, we want the point under
    // the mouse to remain in the same place after the zoom, but this
    // is at mouse/new_scale away from the corner. Therefore we need to
    // shift the origin (coordinates of the corner) to account for this.
    originx -= mousex/(scale*zoom) - mousex/scale;
    originy -= mousey/(scale*zoom) - mousey/scale;
    
    // Scale it (centered around the origin due to the trasnslate above).
    context.scale(zoom, zoom);
    // Offset the visible origin to it's proper position.
    context.translate(-originx, -originy);

    // Update scale and others.
    scale *= zoom;
    visibleWidth = width / scale;
    visibleHeight = height / scale;
}
<canvas id="canvas" width="600" height="200"></canvas>

Ключ, як вказував @Tatarize , полягає в обчисленні положення осі таким чином, що точка масштабування (вказівник миші) залишається там же після масштабування.

Спочатку миша знаходиться на відстані mouse/scaleвід кута, ми хочемо, щоб точка під мишкою залишалася на тому самому місці після збільшення, але це знаходиться mouse/new_scaleдалеко від кута. Тому нам потрібно зрушити origin(координати кута), щоб врахувати це.

originx -= mousex/(scale*zoom) - mousex/scale;
originy -= mousey/(scale*zoom) - mousey/scale;
scale *= zoom

Тоді, що залишився код, потрібно застосувати масштабування та перекласти на контекст малювання, щоб його походження збігалося з кутом полотна.


Дякую чувак, майже загублений 2 дні, перш ніж знайти свій код
Веларо

Гей, я просто шукав щось на кшталт цього і просто хотів сказати, що приголомшив, ти зламав це!
chrisallick

26

Це насправді дуже складна проблема (математично), і я над тим же працюю майже. Я задав подібне запитання щодо Stackoverflow, але не отримав відповіді, але опублікував у DocType (StackOverflow для HTML / CSS) і отримав відповідь. Перевірте це http://doctype.com/javascript-image-zoom-css3-transforms-calculate-origin-example

Я в середині побудови плагіна jQuery, який робить це (масштабування стилів Карт Google за допомогою CSS3 Transforms). У мене курсор миші трохи працює, все ще намагаюся зрозуміти, як дозволити користувачеві перетягувати полотно навколо, як це можна зробити на Картах Google. Коли я працюю, я опублікую тут код, але перевірте вище за посиланням на частину миші та збільшення.

Я не розумів, що існують методи масштабування та перекладу на Canvas контекст, ви можете досягти того ж, використовуючи CSS3, наприклад. використовуючи jQuery:

$('div.canvasContainer > canvas')
    .css('-moz-transform', 'scale(1) translate(0px, 0px)')
    .css('-webkit-transform', 'scale(1) translate(0px, 0px)')
    .css('-o-transform', 'scale(1) translate(0px, 0px)')
    .css('transform', 'scale(1) translate(0px, 0px)');

Переконайтеся, що ви встановили джерело перетворення CSS3 на 0, 0 (-moz-перетворення-походження: 0 0). Використання перетворення CSS3 дозволяє збільшити масштаб будь-чого, просто переконайтеся, що контейнер DIV встановлений для переповнення: прихований, щоб зупинити розширення країв, що розтікаються з боків.

Незалежно від того, чи використовуєте ви перетворення CSS3, або власний масштаб полотна та методи перекладу, вам належить, але перевірте вищенаведене посилання для розрахунків.


Оновлення: Мех! Я просто опублікую код тут, а не змушую вас перейти за посиланням:

$(document).ready(function()
{
    var scale = 1;  // scale of the image
    var xLast = 0;  // last x location on the screen
    var yLast = 0;  // last y location on the screen
    var xImage = 0; // last x location on the image
    var yImage = 0; // last y location on the image

    // if mousewheel is moved
    $("#mosaicContainer").mousewheel(function(e, delta)
    {
        // find current location on screen 
        var xScreen = e.pageX - $(this).offset().left;
        var yScreen = e.pageY - $(this).offset().top;

        // find current location on the image at the current scale
        xImage = xImage + ((xScreen - xLast) / scale);
        yImage = yImage + ((yScreen - yLast) / scale);

        // determine the new scale
        if (delta > 0)
        {
            scale *= 2;
        }
        else
        {
            scale /= 2;
        }
        scale = scale < 1 ? 1 : (scale > 64 ? 64 : scale);

        // determine the location on the screen at the new scale
        var xNew = (xScreen - xImage) / scale;
        var yNew = (yScreen - yImage) / scale;

        // save the current screen location
        xLast = xScreen;
        yLast = yScreen;

        // redraw
        $(this).find('div').css('-moz-transform', 'scale(' + scale + ')' + 'translate(' + xNew + 'px, ' + yNew + 'px' + ')')
                           .css('-moz-transform-origin', xImage + 'px ' + yImage + 'px')
        return false;
    });
});

Звичайно, вам потрібно буде адаптувати його до використання масштабу полотна та методів перекладу.


Оновлення 2: Щойно помічено, я використовую трансформаційне походження разом із перекладом. Мені вдалося реалізувати версію, яка просто використовує масштаб та перекладати самостійно, перегляньте це тут http://www.dominicpettifer.co.uk/Files/Mosaic/MosaicTest.html Зачекайте, коли зображення завантажуватимуться та використовуйте колесо миші для збільшення, також підтримує панорамування, перетягуючи зображення навколо. Він використовує CSS3 Transforms, але ви повинні мати можливість використовувати ті самі розрахунки для свого Canvas.


я, нарешті, вирішив це, зайняв у мене 3 хвилини зараз приблизно через 2
тижні

@Synday Посилання Ironfoot на його оновлення не працює. Це посилання: dominicpettifer.co.uk/Files/Mosaic/MosaicTest.html Я хочу цього імплементації. Чи можете ви розмістити тут код? спасибі
Bogz

2
станом на сьогодні (вересень 2014 р.) посилання на MosaicTest.html померло.
Кріс

демо-мозаїка пішла. Я зазвичай використовую ванільний js, а не jQuery. на що посилається $ (це)? документ.body.offsetTop? Я дуже хочу, щоб мозаїчна демонстрація мого проекту foreverscape.com дійсно могла отримати користь від цього.
FlavorScape

2
Демо-сторінку мозаїки збережено на archive.org: web.archive.org/web/20130126152008/http://…
Кріс

9

Я зіткнувся з цією проблемою за допомогою c ++, що, мабуть, не мав би, я просто використовував матриці OpenGL для початку ... у будь-якому разі, якщо ви використовуєте елемент управління, джерелом якого є верхній лівий кут, і вам потрібно панорамування та збільшення як-от google maps, ось макет (використовуючи allegro як мій обробник подій):

// initialize
double originx = 0; // or whatever its base offset is
double originy = 0; // or whatever its base offset is
double zoom = 1;

.
.
.

main(){

    // ...set up your window with whatever
    //  tool you want, load resources, etc

    .
    .
    .
    while (running){
        /* Pan */
        /* Left button scrolls. */
        if (mouse == 1) {
            // get the translation (in window coordinates)
            double scroll_x = event.mouse.dx; // (x2-x1) 
            double scroll_y = event.mouse.dy; // (y2-y1) 

            // Translate the origin of the element (in window coordinates)      
            originx += scroll_x;
            originy += scroll_y;
        }

        /* Zoom */ 
        /* Mouse wheel zooms */
        if (event.mouse.dz!=0){    
            // Get the position of the mouse with respect to 
            //  the origin of the map (or image or whatever).
            // Let us call these the map coordinates
            double mouse_x = event.mouse.x - originx;
            double mouse_y = event.mouse.y - originy;

            lastzoom = zoom;

            // your zoom function 
            zoom += event.mouse.dz * 0.3 * zoom;

            // Get the position of the mouse
            // in map coordinates after scaling
            double newx = mouse_x * (zoom/lastzoom);
            double newy = mouse_y * (zoom/lastzoom);

            // reverse the translation caused by scaling
            originx += mouse_x - newx;
            originy += mouse_y - newy;
        }
    }
}  

.
.
.

draw(originx,originy,zoom){
    // NOTE:The following is pseudocode
    //          the point is that this method applies so long as
    //          your object scales around its top-left corner
    //          when you multiply it by zoom without applying a translation.

    // draw your object by first scaling...
    object.width = object.width * zoom;
    object.height = object.height * zoom;

    //  then translating...
    object.X = originx;
    object.Y = originy; 
}

9

Мені подобається відповідь Tatarize , але я запропоную альтернативу. Це тривіальна проблема лінійної алгебри, і метод, який я представляю, добре працює з панорами, масштабуванням, перекосом тощо. Тобто він добре працює, якщо ваше зображення вже трансформоване.

Коли масштабується матриця, масштаб знаходиться в точці (0, 0). Отже, якщо у вас є зображення та масштабуйте його на коефіцієнт 2, права нижня точка подвоїться як у напрямку x, так і y (використовуючи умову, що [0, 0] - верхній лівий кут зображення).

Якщо замість цього ви хочете збільшити масштаб зображення в центрі, то рішення полягає в наступному: (1) переведіть зображення таким чином, щоб його центр був у (0, 0); (2) масштабувати зображення за x та y коефіцієнтами; (3) перевести зображення назад. тобто

myMatrix
  .translate(image.width / 2, image.height / 2)    // 3
  .scale(xFactor, yFactor)                         // 2
  .translate(-image.width / 2, -image.height / 2); // 1

Більш абстрактно, та ж стратегія працює для будь-якої точки. Наприклад, якщо ви хочете масштабувати зображення в точці P:

myMatrix
  .translate(P.x, P.y)
  .scale(xFactor, yFactor)
  .translate(-P.x, -P.y);

І нарешті, якщо зображення вже певним чином трансформоване (наприклад, якщо воно повертається, перекошується, перекладається чи масштабується), то поточне перетворення потрібно зберегти. Зокрема, визначене вище перетворення повинно бути помножене (або помножене право) поточним перетворенням.

myMatrix
  .translate(P.x, P.y)
  .scale(xFactor, yFactor)
  .translate(-P.x, -P.y)
  .multiply(myMatrix);

Там у вас є. Ось планк, який показує це в дії. Прокрутіть курсор миші на крапках, і ви побачите, що вони послідовно тримаються. (Тестується лише в Chrome.) Http://plnkr.co/edit/3aqsWHPLlSXJ9JCcJzgH?p=preview


1
Треба сказати, якщо у вас є доступна матриця афінної трансформації, використовуйте це з ентузіазмом. Багато матриць перетворення навіть матимуть функції збільшення (sx, sy, x, y), які роблять саме це. Практично варто приготувати його, якщо його не дають використовувати.
Татариз

Насправді, я визнаю, що в коді, в якому я використовував це рішення, з тих пір було замінено його на клас матриць. І я робив цю точну річ кілька разів і готував матричні класи не менше двох разів. ( github.com/EmbroidePy/pyembroidery/blob/master/pyembroidery/… ), ( github.com/EmbroidePy/EmbroidePy/blob/master/embroidepy/… ). Якщо ви хочете нічого складнішого, ніж саме ці операції, матриця - це в основному правильна відповідь, і як тільки ви отримаєте ручку лінійної алгебри, ви зрозумієте, що ця відповідь - це найкраща відповідь.
Татариз

6

Ось моє рішення для орієнтованого на центр зображення:

var MIN_SCALE = 1;
var MAX_SCALE = 5;
var scale = MIN_SCALE;

var offsetX = 0;
var offsetY = 0;

var $image     = $('#myImage');
var $container = $('#container');

var areaWidth  = $container.width();
var areaHeight = $container.height();

$container.on('wheel', function(event) {
    event.preventDefault();
    var clientX = event.originalEvent.pageX - $container.offset().left;
    var clientY = event.originalEvent.pageY - $container.offset().top;

    var nextScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, scale - event.originalEvent.deltaY / 100));

    var percentXInCurrentBox = clientX / areaWidth;
    var percentYInCurrentBox = clientY / areaHeight;

    var currentBoxWidth  = areaWidth / scale;
    var currentBoxHeight = areaHeight / scale;

    var nextBoxWidth  = areaWidth / nextScale;
    var nextBoxHeight = areaHeight / nextScale;

    var deltaX = (nextBoxWidth - currentBoxWidth) * (percentXInCurrentBox - 0.5);
    var deltaY = (nextBoxHeight - currentBoxHeight) * (percentYInCurrentBox - 0.5);

    var nextOffsetX = offsetX - deltaX;
    var nextOffsetY = offsetY - deltaY;

    $image.css({
        transform : 'scale(' + nextScale + ')',
        left      : -1 * nextOffsetX * nextScale,
        right     : nextOffsetX * nextScale,
        top       : -1 * nextOffsetY * nextScale,
        bottom    : nextOffsetY * nextScale
    });

    offsetX = nextOffsetX;
    offsetY = nextOffsetY;
    scale   = nextScale;
});
body {
    background-color: orange;
}
#container {
    margin: 30px;
    width: 500px;
    height: 500px;
    background-color: white;
    position: relative;
    overflow: hidden;
}
img {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    max-width: 100%;
    max-height: 100%;
    margin: auto;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

<div id="container">
    <img id="myImage" src="http://s18.postimg.org/eplac6dbd/mountain.jpg">
</div>


4

Ось альтернативний спосіб зробити це, який використовує setTransform () замість шкали () та перекладати (). Все зберігається в одному об’єкті. Полотно вважається рівним 0,0 на сторінці, інакше потрібно буде відняти його положення від сторінок.

this.zoomIn = function (pageX, pageY) {
    var zoomFactor = 1.1;
    this.scale = this.scale * zoomFactor;
    this.lastTranslation = {
        x: pageX - (pageX - this.lastTranslation.x) * zoomFactor,
        y: pageY - (pageY - this.lastTranslation.y) * zoomFactor
    };
    this.canvasContext.setTransform(this.scale, 0, 0, this.scale,
                                    this.lastTranslation.x,
                                    this.lastTranslation.y);
};
this.zoomOut = function (pageX, pageY) {
    var zoomFactor = 1.1;
    this.scale = this.scale / zoomFactor;
    this.lastTranslation = {
        x: pageX - (pageX - this.lastTranslation.x) / zoomFactor,
        y: pageY - (pageY - this.lastTranslation.y) / zoomFactor
    };
    this.canvasContext.setTransform(this.scale, 0, 0, this.scale,
                                    this.lastTranslation.x,
                                    this.lastTranslation.y);
};

Супровідний код для обробки панорами:

this.startPan = function (pageX, pageY) {
    this.startTranslation = {
        x: pageX - this.lastTranslation.x,
        y: pageY - this.lastTranslation.y
    };
};
this.continuePan = function (pageX, pageY) {
    var newTranslation = {x: pageX - this.startTranslation.x,
                          y: pageY - this.startTranslation.y};
    this.canvasContext.setTransform(this.scale, 0, 0, this.scale,
                                    newTranslation.x, newTranslation.y);
};
this.endPan = function (pageX, pageY) {
    this.lastTranslation = {
        x: pageX - this.startTranslation.x,
        y: pageY - this.startTranslation.y
    };
};

Щоб отримати відповідь самостійно, врахуйте, що одні й ті ж координати сторінки повинні відповідати однаковим координатам полотна до та після збільшення. Тоді ви можете зробити алгебру, починаючи з цього рівняння:

(pageCoords - переклад) / scale = canvasCoords


3

Я хочу покласти сюди деяку інформацію для тих, хто окремо малює малюнок і рухається -намалює його.

Це може бути корисно, коли ви бажаєте зберегти масштаби та положення вікна перегляду.

Ось ящик:

function redraw_ctx(){
   self.ctx.clearRect(0,0,canvas_width, canvas_height)
   self.ctx.save()
   self.ctx.scale(self.data.zoom, self.data.zoom) // 
   self.ctx.translate(self.data.position.left, self.data.position.top) // position second
   // Here We draw useful scene My task - image:
   self.ctx.drawImage(self.img ,0,0) // position 0,0 - we already prepared
   self.ctx.restore(); // Restore!!!
}

Помітьте, що шкала ПОВИННА бути першою .

І ось зумер:

function zoom(zf, px, py){
    // zf - is a zoom factor, which in my case was one of (0.1, -0.1)
    // px, py coordinates - is point within canvas 
    // eg. px = evt.clientX - canvas.offset().left
    // py = evt.clientY - canvas.offset().top
    var z = self.data.zoom;
    var x = self.data.position.left;
    var y = self.data.position.top;

    var nz = z + zf; // getting new zoom
    var K = (z*z + z*zf) // putting some magic

    var nx = x - ( (px*zf) / K ); 
    var ny = y - ( (py*zf) / K);

    self.data.position.left = nx; // renew positions
    self.data.position.top = ny;   
    self.data.zoom = nz; // ... and zoom
    self.redraw_ctx(); // redraw context
    }

і, звичайно, нам знадобиться драггер:

this.my_cont.mousemove(function(evt){
    if (is_drag){
        var cur_pos = {x: evt.clientX - off.left,
                       y: evt.clientY - off.top}
        var diff = {x: cur_pos.x - old_pos.x,
                    y: cur_pos.y - old_pos.y}

        self.data.position.left += (diff.x / self.data.zoom);  // we want to move the point of cursor strictly
        self.data.position.top += (diff.y / self.data.zoom);

        old_pos = cur_pos;
        self.redraw_ctx();

    }


})

3
if(wheel > 0) {
    this.scale *= 1.1; 
    this.offsetX -= (mouseX - this.offsetX) * (1.1 - 1);
    this.offsetY -= (mouseY - this.offsetY) * (1.1 - 1);
}
else {
    this.scale *= 1/1.1; 
    this.offsetX -= (mouseX - this.offsetX) * (1/1.1 - 1);
    this.offsetY -= (mouseY - this.offsetY) * (1/1.1 - 1);
}

2

Ось реалізація коду відповіді @ tatarize за допомогою PIXI.js. У мене є огляд огляду частини дуже великого зображення (наприклад, стиль Google Maps).

$canvasContainer.on('wheel', function (ev) {

    var scaleDelta = 0.02;
    var currentScale = imageContainer.scale.x;
    var nextScale = currentScale + scaleDelta;

    var offsetX = -(mousePosOnImage.x * scaleDelta);
    var offsetY = -(mousePosOnImage.y * scaleDelta);

    imageContainer.position.x += offsetX;
    imageContainer.position.y += offsetY;

    imageContainer.scale.set(nextScale);

    renderer.render(stage);
});
  • $canvasContainer мій контейнер html.
  • imageContainer це мій контейнер PIXI, у якому є зображення.
  • mousePosOnImage - це положення миші відносно всього зображення (не тільки порту перегляду).

Ось як я отримав положення миші:

  imageContainer.on('mousemove', _.bind(function(ev) {
    mousePosOnImage = ev.data.getLocalPosition(imageContainer);
    mousePosOnViewport.x = ev.data.originalEvent.offsetX;
    mousePosOnViewport.y = ev.data.originalEvent.offsetY;
  },self));

0

Вам потрібно отримати точку у світовому просторі (на відміну від екранного простору) до та після масштабування, а потім перекласти на дельту.

mouse_world_position = to_world_position(mouse_screen_position);
zoom();
mouse_world_position_new = to_world_position(mouse_screen_position);
translation += mouse_world_position_new - mouse_world_position;

Положення миші знаходиться в просторі екрану, тому вам доведеться перетворити його на світовий простір. Просте перетворення має бути подібним до цього:

world_position = screen_position / scale - translation

0

ви можете використовувати функцію scrollto (x, y) для обробки положення смуги прокрутки вправо до тієї точки, яку вам потрібно показати після збільшення. Для пошуку позиції миші використовуйте event.clientX та event.clientY. це вам допоможе


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.